【C++】线程库

一、thread类

线程库是C++11标准提出来的,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。
在这里插入图片描述

常见接口:

函数名 功能
thread() 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
get_id() 获取线程id
jion() 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach() 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关

注意:
1️⃣ 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的
状态。
2️⃣ 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
3️⃣ get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类。
4️⃣ 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:
函数指针
lambda表达式
函数对象

使用:

#include <iostream>
#include <thread>
#include <vector>
#include <windows.h>

using namespace std;

void fun()
{
    
    
	Sleep(1100);
	cout << this_thread::get_id() << endl;
}

int main()
{
    
    
	thread t1([]() {
    
    
		while (true)
		{
    
    
			Sleep(1000);
			cout << this_thread::get_id() << endl;
		}
		});
	thread t2(fun);

	t1.join();
	t2.join();
	return 0;
}

在这里插入图片描述

二、线程安全问题

static int val = 0;

void fun1(int n)
{
    
    
	for (int i = 0; i < n; i++)
	{
    
    
		val++;
	}
}

void fun2(int n)
{
    
    
	for (int i = 0; i < n; i++)
	{
    
    
		val++;
	}
}

int main()
{
    
    
	thread t1(fun1, 100000);
	thread t2(fun2, 200000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

在这里插入图片描述
可以看到本来应该加到300000,现在却没有加到。
因为val++操作并不是原子的。

2.1 加锁

为了保证线程安全我们需要加锁:

static int val = 0;
mutex mtx;

void fun1(int n)
{
    
    
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
    
    
		val++;
	}
	mtx.unlock();
}

void fun2(int n)
{
    
    
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
    
    
		val++;
	}
	mtx.unlock();
}

int main()
{
    
    
	thread t1(fun1, 100000);
	thread t2(fun2, 200000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

在这里插入图片描述
这里注意加锁和解锁放在for循环外边比较好,因为加锁和解锁的过程也是要消耗资源的。

这里如果两个线程同时调用一个函数也可以。
在这里插入图片描述
这里的原因是每个线程都会有独立的栈结构来保存私有数据。

2.2 CAS操作

CAS全称compare and swap,JDK提供的非阻塞原子性操作,它通过硬件保证了更新操作的原子性。它允许多线程非阻塞地对共享资源进行修改,但是同一时刻只有一个线程可以修改,其他线程并不会阻塞而是重新尝试。

在这里插入图片描述

2.3 原子性操作库(atomic)

原子性操作库就提供了CAS的相关接口。
如果要使用先得引入头文件:

#include <atomic>
atomic<int> val = 0;

void fun1(int n)
{
    
    
	for (int i = 0; i < n; i++)
	{
    
    
		val++;
	}
}

void fun2(int n)
{
    
    
	for (int i = 0; i < n; i++)
	{
    
    
		val++;
	}
}

int main()
{
    
    
	thread t1(fun1, 100000);
	thread t2(fun1, 200000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

在这里插入图片描述

这里有val++会被放弃,以此来保证线程安全

而在实际中要尽量避免使用全局变量。

int main()
{
    
    
	atomic<int> val = 0;
	auto func = [&](int n) {
    
    
		for (int i = 0; i < n; i++)
		{
    
    
			val++;
		}
	};
	thread t1(func, 10000);
	thread t2(func, 20000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

三、锁

3.1 lock与try_lock的区别

lock的加锁过程:如果没有锁就申请锁,如果其他线程持有锁就会阻塞等待,知道其他线程unlock。
而try_lock就可以不让线程阻塞,如果申请不了就可以去干其他的事情。成功返回true,失败返回false。

if (try_lock())
{
    
    
	// ...
}
else
{
    
    
	// 干其他的事情
}

3.2 recursive_mutex递归锁

如果在递归函数中我们想要正常用lock加锁,很可能能会导致死锁。因为上锁后递归到下一层,锁并没有被解开,相当于自己上了锁以后又申请锁。

void fun()
{
    
    
	lock();
	fun();// 递归
	unlock();
}

而使用recursive_mutex就可以避免这种情况。
原理:

递归到下一层后遇到加锁,就先判断线程的id值,如果一样就不用加锁,直接走接下来的流程。

3.3 lock_guard RAII锁

什么是RAII?

是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。

我们知道有可能加锁后会抛出异常,那么就可能会导致锁没有被释放。为了避免这种情况,我们可以把锁封装一下,在析构函数中就可以加上解锁,这样出了作用域就可以自动销毁。
具体的实现在【linux】线程的互斥与同步 2.5 锁的封装

线程库给我们提供了这样一把锁
在这里插入图片描述

int main()
{
    
    
	int val = 0;
	mutex mtx;
	auto func = [&](int n) {
    
    
		lock_guard<mutex> lock(mtx);
		for (int i = 0; i < n; i++)
		{
    
    
			val++;
		}
	};
	thread t1(func, 10000);
	thread t2(func, 20000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

在这里插入图片描述

3.4 unique_lock主动解锁

在这里插入图片描述
它和lock_guard的区别就是lock_guard只能实现RAII,而unique_lock能主动把自己的锁解开,不用等到析构。

四、两个线程交替打印1~100

现在我们想让两个线程交替打印从1到100,一个线程打印奇数,一个线程打印偶数。

int main()
{
    
    
	int val = 1;
	thread t1([&]() {
    
    
		while (val < 100)
		{
    
    
			if (val % 2 != 0)
			{
    
    
				cout << "thread 1" << "->" << val << endl;
				val++;
			}
		}
		});
	thread t2([&]() {
    
    
		while (val <= 100)
		{
    
    
			if (val % 2 == 0)
			{
    
    
				cout << "thread 2" << "->" << val << endl;
				val++;
			}
		}
		});
	t1.join();
	t2.join();
	return 0;
}

虽然这可以做到要求,但是可能会造成资源浪费。

有这样一种场景,t2满足条件正在运行,但是时间片到了,切换到t1,此时t1不满足条件,一直在while处死循环,知道时间片到了才切换出去。
这样就会导致浪费CPU资源。

所以我们希望两个线程能够相互通知,这就需要条件变量控制。

4.1 条件变量

关于条件变量的概念在【linux】线程的互斥与同步 3.1 条件变量里面有详细介绍。

而C++11也对条件变量进行了封装。
头文件:#include <condition_variable>
相关接口:
在这里插入图片描述

在这里插入图片描述
而我们知道条件变量不是线程安全的,所以要先加一把锁。
这里注意使用wait的时候必须把锁传递进去,而且必须是unique_lock
wait把锁传进去是为了解锁,返回时才会重新上锁。

int main()
{
    
    
	int val = 1;
	mutex mtx;
	condition_variable cv;
	thread t1([&]() {
    
    
		while (val < 100)
		{
    
    
			unique_lock<mutex> lock(mtx);
			while (val % 2 == 0)
			{
    
    
				cv.wait(lock);// 阻塞
			}
			cout << "thread 1" << "->" << val << endl;
			val++;
			cv.notify_one();
		}
		});
	thread t2([&]() {
    
    
		while (val <= 100)
		{
    
    
			unique_lock<mutex> lock(mtx);
			while (val % 2 != 0)
			{
    
    
				cv.wait(lock);
			}
			cout << "thread 2" << "->" << val << endl;
			val++;
			cv.notify_one();
		}
		});
	t1.join();
	t2.join();
	return 0;
}

分析:

刚开始t1申请到锁,那么t2就会在申请锁的地方阻塞等待。但是t1却不满足条件,所以进行wait等待,进入wait函数会自动解锁,那么t2就可以运行了。这里注意notify_one()可能要唤醒的是一个正在运行的线程,但是没问题,此时notify_one()默认什么都不会做。



猜你喜欢

转载自blog.csdn.net/qq_66314292/article/details/130173588