【C++】C++11之线程库


目录

一、thread类

1.1 thread类的构造方法

1.2 其他函数接口

注意

2.3 this_thread命名空间

二、mutex锁

2.1 mutex类

2.2 recursive_mutex

2.3 timed_mutex

三、原子性操作库(atomic)

四、利用RAII机制管理锁

4.1 lock_guard

4.2 unique_lock

五、条件变量


一、thread类

C++11 之前,涉及到多线程问题,都是和平台相关的,比如 windows linux 下各有自己的接 口,这使得代码的可移植性比较差 C++11 中最重要的特性就是对线程进行支持了,使得 C++ 并行编程时不需要依赖第三方库 ,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread > 头文件。

  

我们可以参考下面文档:C++ thread类

   

1.1 thread类的构造方法

1、支持无参构造。构造一个空线程对象,由于没有关联的线程函数,所以不会直接运行。

2、支持可变参数构造。(最常用) 构造一个线程对象,并关联线程函数funargs1args2...为线程函数的参数。

#include<iostream>
#include<thread>

using namespace std;

void Add(int x, int y)
{
	cout << x + y << endl;
}

int main()
{
	int a = 10, b = 30;
	thread t1(Add, a, b);
	t1.join();
	return 0;
}

这里join函数的作用是让线程运行完进程进行回收。不然就会造成资源不回收,引发内存泄漏。 

3、不支持拷贝构造。

 

4、支持移动赋值。

   

1.2 其他函数接口

get_id:获取线程id,也是线程的唯一标识。get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体:

typedef struct
{     /* thread identifier for Win32 */
     void *_Hnd; /* Win32 HANDLE */
     unsigned int _Id;
} _Thrd_imp_t;

join:等待线程回收分配给线程的资源。

joinable:用于判断是否需要回收线程资源。                  

detach:线程与主线程分离,彼此独立运行。两个线程继续,不会以任何方式阻塞或同步。请注意,当任何一个结束执行时,都会释放其自己的资源。

  

注意

1. 线程是操作系统中的一个概念, 线程对象可以关联一个线程,用来控制线程以及获取线程的状态
2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
3. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。
线程函数一般情况下可按照以下三种方式提供:
  • 函数指针
  • lambda表达式
  • 函数对象(仿函数)
class temp
{
public:
	void operator()()
	{
		cout << "thread t3" << endl;
	}
};

int main()
{
	thread t1(Add, 1, 2);
	thread t2([]() {cout << "thread t2" << endl; });
	temp t;
	thread t3(t);

	t1.join();
	t2.join();
	t3.join();
	return 0;
}
4. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
5. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
  • 采用无参构造函数构造的线程对象
  • 线程对象的状态已经转移给其他线程对象
  • 线程已经调用jion或者detach结束

    

2.3 this_thread命名空间

 get_id:用于获取线程id。

sleep_for:进程睡眠一段时间。

sleep_until:进程睡眠至某个时间。

由于不会特别常用,这里就不详细介绍,需要用时差文档即可:this_thread - C++ Reference (cplusplus.com)


二、mutex锁

2.1 mutex类

多线程最主要的问题是共享数据带来的问题(即线程安全) 如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦
比如:
我们在实现++操作的时候,看起来是一行代码,实际上底层汇编有三条。当我们进行到一半而时间片的时间到了,那么该线程就会被切走阻塞,让别的线程来使用cpu,而如果后来的线程也对a进行++操作,操作后再把原来的进程切换回来,原来的进程操作的还是原来的a,那么最后的结果就会出现问题。

案例代码:

int ret = 0;

void Func()
{ 
	int n = 10000;
	while (n--)
	{
		ret++;
	}
}

int main()
{
	thread t1(Func);
	thread t2(Func);

	t1.join();
	t2.join();

	cout << ret << endl;

	return 0;
}

 为了解决这个问题,引入了锁mutex来使得++操作一次完成。

 mutex类用到的主要两个函数就是:lock 和 unlock

mutex m;
int ret = 0;

void Func()
{ 
	int n = 10000;
	while (n--)
	{
		m.lock();
		ret++;
		m.unlock();
	}
}

int main()
{
	thread t1(Func);
	thread t2(Func);

	t1.join();
	t2.join();

	cout << ret << endl;

	return 0;
}

    

2.2 recursive_mutex

mutex类的锁是不能够递归加锁的,会出问题。为了适应这种情况,引入了recursive_mutex类。

 该类提供的函数接口和mutex类一样,但是允许一个线程多次加锁,来获得互斥对象的多个级别的所有权。

  

2.3 timed_mutex

相较于上面两种锁,timed_mutex锁增加了两个功能:try_lock_for 和 try_lock_until 

try_lock:能够在一定的时间范围内申请锁。如果当前锁未被申请,那么调用的线程就将取走锁;如果当前锁已经被申请了,那么就会返回false。

try_lock_until:尝试申请锁知道某个时间点。


三、原子性操作库(atomic)

虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对 sum++ 时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作, C++11 引入的原子操作类型,使得线程间数据的同步变得非常高效。

该类的使用需要包含头文件<atomic>

 

我们下面看atomic类的构造方法:

可以看到:支持无参构造和列表初始化,但是不能拷贝。

原子类型通常属于 " 资源型 " 数据,多个线程只能访问单个原子类型的拷贝,因此 C++11 中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及 operator= 等,为了防止意外,标准库已经将 atmoic 模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。
  
使用案例:
atomic<int> a;
void func()
{
	int n = 10000;
	while (n--)
	{
		a++;
	}
}

int main()
{
	thread t1(func);
	thread t2(func);

	t1.join();
	t2.join();

	cout << a << endl;

	return 0;
}

C++11 中, 程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的 访问 。更为普遍的,程序员可以使用 atomic 类模板,定义出需要的任意原子类型

四、利用RAII机制管理锁

4.1 lock_guard

这是一个C++中定义的用来管理锁的类,在构造对象时候加锁,析构对象的时候解锁。

实现代码:

template<class _Mutex>
class lock_guard
{
public:
	explicit lock_guard(_Mutex& _Mtx)
		:_MyMutex(_Mtx)
	{
		_MyMutex.lock();
	}

	lock_guard(_Mutex& _Mtx, adopt_lock_t)
		:_MyMutex(_Mtx)
	{}

	~lock_guard() _NOEXCEPT
	{
		_MyMutex.unlock();
	}

	lock_guard(const lock_guard&) = delete;
	lock_guard& operato = (const lock_guard&) = delete;
private:
	_Mutex _MyMutex;
};

通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。 

案例:

int a = 0;
mutex mx;

void func()
{
	int n = 10000;
	lock_guard<mutex> mt(mx);
	while (n--)
	{
		a++;
	}
}

int main()
{
	thread t1(func);
	thread t2(func);

	t1.join();
	t2.join();

	cout << a << endl;

	return 0;
}

     

4.2 unique_lock

lock_gard 类似, unique_lock 类模板也是采用 RAII 的方式对锁进行了封装,并且也是以独占所 有权的方式管理 mutex 对象的上锁和解锁操作,即其对象之间不能发生拷贝 。在构造 ( 或移动(move)赋值 ) 时, unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。 使用以上类型互斥量实例化 unique_lock 的对象时,自动调用构造函数上锁, unique_lock 对象销毁时自动调用析构函数解 锁,可以很方便的防止死锁问题。
  
lock_guard 不同的是, unique_lock 更加的灵活,提供了更多的成员函数:
  • 上锁/解锁操作locktry_locktry_lock_fortry_lock_untilunlock
  • 修改操作移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
  • 获取属性owns_lock(返回当前对象是否上了锁)operator bool()(owns_lock()的功能相同)mutex(返回当前unique_lock所管理的互斥量的指针)

五、条件变量

我们先看一道题:两个线程交替打印0-100的数字,一个打印奇数,一个打印偶数。

我们通常情况下解题是while循环中用if条件判断来判断,一个线程t1判断奇数打印,一个线程t2判断偶数打印,然后打印完++。但是当我们打印t1奇数的时候,此时时间片切到t2,t2会不断的循环判断,直到时间片切回t1。这样就造成了CPU资源的浪费。

这里就要引入我们的条件变量:std::condition_variable、

条件变量中的 wait 和 notify_one 的接口能够实现进程的等待和唤醒。 使得进程避免因为不满足条件而一直循环判断,浪费资源。

 需要注意的是:

wait接口的参数是unique_lock类型。

有人会好奇为什么需要传一个锁进来呢?

因为条件变量操作不是原子性的,我们需要加锁保护,但是我们加了锁让线程等待,但是其他线程因为申请不到锁也会进入阻塞,那么不就死循环了吗?

其实并不是的,wait操作之所以需要传一个锁进来,就是因为wait操作的同时,会将锁释放,让其他线程能够申请到锁,直到用notify_one来唤醒线程的时候,才会重新持有锁。

   

有了条件变量,我们可以让进程在不满足条件的时候进行等待,在满足条件之后再唤醒进程运行。

案例代码:

int main()
{
	int a = 0;
	condition_variable cv;
	mutex mt;
	//打印奇数
	thread t1([&]()
		{
			while (a <= 100)
			{
				unique_lock<mutex> lock(mt);
				if (a % 2 == 0)
				{
					cv.wait(lock);
				}
				cout << "t1->" << a << endl;
				++a;

				cv.notify_one();
			}
		});

	thread t2([&]()
		{
			while (a <= 100)
			{
				unique_lock<mutex> lock(mt);
				if (a % 2 == 1)
				{
					cv.wait(lock);
				}
				cout << "t2->" << a << endl;
				++a;

				cv.notify_one();
			}
		});

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

我们这里就发现问题了,怎么会打印出101来呢?

原因出在这里:

 因此我们只需要把t1时的循环条件<= 改成 < 即可,这样,在100的时候进不去循环了,自然后面的操作也就不会执行了。

猜你喜欢

转载自blog.csdn.net/qq_65139309/article/details/131585940