C++11的线程库

前言:

C++ 11通过标准库引入了对多线程的支持,这个是c++的新特性之一,也就是说我们直接用即可,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念(这个后文会讲到)。线程啥的就不再解释了,直接上干货;

头文件一定记得写如下几个:

#include <thread>   //线程库 #include <condition_variable>     //条件变量 #include <mutex>    //互斥锁 

1. 线程库函数的使用:

函数 功能
thread() 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn,args1, args2,…) 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数
get_id() 获取线程id
jionable() 线程是否是有效的,joinable代表的是一个正在执行中的线程。
join() 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach() 在创建线程对象后马上调用,用于把被创建线程与主线程分离,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关。

注意:

  1. 线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
  2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
  3. 线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,其实际引用的是线程栈中的拷贝,而不是外部实参。(也就是说创建thread对象进行绑定时,哪怕你的形参事引用都不会改变当前函数的变量值,有例子)
  4. 一个线程对象只能使用一次join(),不然程序会崩溃;在线程对象销毁前,要么以jion()的方式等待线程结束,要么以detach()的方式。

样例1(对应上面第三点):

void Fun1(int& x) { 	x += 20; } void Fun2(int* x) { 	*x += 20; }  int main() { 	int a = 10;  	thread t1(Fun1, a); 	t1.join(); 	//线程函数参数尽管是引用方式,实际引用的是线程栈中的拷贝 	cout << a << endl;                         // 10   	// 如果想要通过形参改变外部实参时,怎么办呢?这时借助std::ref()函数 	thread t3(Fun1, std::ref(a)); 	t3.join(); 	cout << a << endl;            //30  	thread t2(Fun2, &a); 	t2.join(); 	cout << a << endl;          //50  	return 0; }  

2. 原子操作

C++11标准定义“原子类型”,可以保证原子类型在线程间被互斥的访问。

    atomic_bool     abool;              //对应bool     atomic_char     achar;              //char     atomic_schar    aschar;             //signed char     atomic_uchar    auchar;             //unsigned char     atomic_int      aint;               //int     atomic_uint     auint;              //unsigned int     atomic_short    ashort;             //short     atomic_ushort   aushort;            //unsigned short     atomic_long     along;              //long     atomic_ulong    aulong;             //unsigned long     atomic_llong    allong;             //long long     atomic_ullong   aullong;            //unsigned long long     atomic_char16_t achar16_t;          //char16_t     atomic_char32_t achar32_t;          //char32_t;     atomic_wchar_t  awchar_t;           //wchar_t 

但是,我们应该使用atomic类模板。通过该模板,可以定义出任意需要的原子类型:

std::atomic< type > t;

对线程而言,原子类型通常属于“资源型”的数据,这意味着多个线程通常只能访问的原子类型的拷贝。所以在C++11中,原子类型只能从其模板参数类型中进行构造,标准不允许原子类型经行拷贝构造、移动构造,以及operator=等,防止以外发生;

举个例子:

atomic< float > af{ 1.2f };
//atomic< float > af1{ af }; //这里无法编译
原因:atomic模板类的拷贝构造函数、移动构造函数、operator=等总是默认被删除的

在C++11中,标准将原子操作定义为atomic模板类的成员函数,这囊括了绝大多数典型的操作,如读、写、交换等,当然,对于内置类型而言,主要是通过重载一些全局操作符来完成的,在编译的时候,会产生一条特殊的lock前缀的x86指令,lock能够控制总线及实现x86平台上的原子性。

C++11的线程库

上面的那些是原子类型的函数的操作:读(load)、写(store)、交换(exchange)、比较并交换(compare_exchange_weak/compare_exchange_stronge)等操作;

当然,有时编译器会给我们作出优化:

	atomic<int> a;     a = 1;          //a.store(1);     int b = a;      //b = a.load(); 

上图中,那个atomic_flag,这个要特别关注一下,听说效率很高,可以自制自旋锁,如下:

void Lock(atomic_flag *lock)    { while (lock->test_and_set()); } void Ublock(atomic_flag *lock)  { lock->clear(); } //test_and_set()函数是设置true值,返回之前的值。 //clear()是复位,置为false; 

std::atomic_flag lock = ATOMIC_FLAG_INIT; //初始化

代码演示:

// 自旋锁实现.cpp : 定义控制台应用程序的入口点。 //  #include <iostream> #include <atomic> #include <thread> #include <Windows.h> using namespace std; std::atomic_flag lock = ATOMIC_FLAG_INIT;               //声明了全局变量,初始化为值ATOMIC_FLAG_INIT,即false状态 void Lock(atomic_flag *lock)    { while (lock->test_and_set()){     cout<<"Waiting..."<<endl; } } void Ublock(atomic_flag *lock)  { lock->clear(); }   void func(){     Lock(&lock);     cout << "func working..." << endl;     Ublock(&lock); } void foo(){     Lock(&lock);     cout<<"foo working..."<<endl;     Ublock(&lock); } int main(void) {     std::thread t1(func);     std::thread t2(foo);     t1.join();     t2.join();      system("pause");     return 0; } 

截图:
C++11的线程库

原子类型有一些枚举值,这个可以稍微了解一下。
C++11的线程库

高级用法:

一、多线程启动函数:std::atemplate <class Fn, class... Args> future<typename result_of<Fn(Args...)>::type> async (launch policy, Fn&& fn, Args&&... args);

其中:

// 异步启动的策略   enum class launch {       // 异步启动,在调用std::async()时创建一个新的线程以异步调用函数,并返回future对象;     async = 0x1,     // 延迟启动,在调用std::async()时不创建线程,直到调用了future对象的get()或wait()方法时,才创建线程;                         deferred = 0x2,        // 自动,函数在某一时刻自动选择策略,这取决于系统和库的实现,通常是优化系统中当前并发的可用性                any = async | deferred,           sync = deferred   };  //参数 fn 是要调用的可调用 (Callable) 对象 //参数args 是传递给 f 的参数  //std::launch::async:在调用async就开始创建线程。 //std::launch::deferred:延迟加载方式创建线程。调用async时不创建线程,直到调用了future的get或者wait时才创建线程。 
异步调用,当然可以大大提高程序运行的效率~

std::async()是一个接受回调(函数或函数对象)作为参数的函数模板,通过启动一个新线程或者复用一个它认为合适的已有线程异步调用。

std::async返回一个std::future< T >,它存储由std::async()执行的函数对象返回的值。所以通常都有std::future伴随着使用,因为future中存储了线程函数返回的结果。

内部原理:
std::async先异步操作用std::p//查询future的状态 std::future_status status; do { status = future.wait_for(std::chrono::seconds(1)); //等待一秒 if (status == std::future_status::deferred) { std::cout << "deferredn"; } else if (status == std::future_status::timeout) { std::cout << "timeoutn"; } else if (status == std::future_status::ready) { std::cout << "ready!n"; } } while (status != std::future_status::ready);

std::chrono知识点

代码演示:

# include <iostream> # include <ctime> # include <future> # include <thread>  using namespace std;   int funca(int a,int b){     return a+b; } int funcb(int a){     return a; }  int main(void) {     future<int> f1 = std::async(funca,1,2);    //<type>   是绑定的函数返回值 	future<int> f2 = std::async(funcb,3);      auto it = f1.get() + f2.get();     cout<<it<<endl;          system("pause");     return 0; } 
三、std::promise

std::promise可以获取线程函数里的值,不过要等执行完毕后才可以获取;当然,是间接地通过promise内部提供的future来获取的!

用法:

 std::promise<int> pr; std::thread t([](std::promise<int>& p){ p.set_value_at_thread_exit(9); },std::ref(pr)); std::future<int> f = pr.get_future();  

点击了解

四、std::packaged_task

这个是包装了一个可调用对象(如function, lambda expression, bind expression, or another function object);packaged_task保存的是一个函数。
用法:

std::packaged_task<int()> task([](){ return 7; }); std::thread t1(std::ref(task));  std::future<int> f1 = task.get_future();  auto r1 = f1.get(); 
注:一般来说,用std::future以及std::async这两个用法即可。

锁: 最常见的就是mutex (有RAII思想的管理锁的类模板,可以预防我们忘记解锁)

C++11根据mutext的属性提供四种的互斥量,分别是:

  1. std::mutex,最常用,普遍的互斥量(默认属性),
  2. std::recursive_mutex ,递归锁,允许同一线程使用recursive_mutext多次加锁,然后使用相同次数的解锁操作解锁。mutex多次加锁会造成死锁
  3. std::timed_mutex,在mutex上增加了时间的属性。增加了两个成员函数try_lock_for(),try_lock_until(),分别接收一个时间范围,再给定的时间内如果互斥量被锁主了,线程阻塞,超过时间,返回false。
  4. std::recursive_timed_mutex,增加递归和时间属性
C++11的线程库

时间锁:

timed_mutex myMutex; chrono::milliseconds timeout(1000);  //1秒 if (myMutex.try_lock_for(timeout)) { 	//在1秒内获取了锁 	//业务代码 	myMutex.unlock();  } else { 	//在100毫秒内没有获取锁 	//业务代码 } 

time_mutex博客

mutex成员函数(常用):
6. lock(),互斥量加锁,如果互斥量已被加锁,线程阻塞
7. bool try_lock(),尝试加锁,如果互斥量未被加锁,则执行加锁操作,返回true;如果互斥量已被加锁,返回false,线程不阻塞。
8. void unlock(),解锁互斥量

C++11的线程库

mutex RAII式的加锁解锁

std::lock_guard

管理mutex的类。以独占所有权的方式管理mutex对象的上锁和解锁操作,对象构建时传入mutex,会自动对mutex加入,直到离开类的作用域,析构时完成解锁。RAII式的栈对象能保证在异常情形下mutex可以在lock_guard对象析构被解锁。

C++11的线程库
源码:

template<class _Mutex> class lock_guard { public: 	// 在构造lock_gard时,_Mtx还没有被上锁 	explicit lock_guard(_Mutex& _Mtx) 		: _MyMutex(_Mtx) 	{ 		_MyMutex.lock(); 	} 	// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁 	lock_guard(_Mutex& _Mtx, adopt_lock_t) 		: _MyMutex(_Mtx) 	{} 	~lock_guard()  	{ 		_MyMutex.unlock(); 	} 	lock_guard(const lock_guard&) = delete; 	lock_guard& operator=(const lock_guard&) = delete; private: 	_Mutex& _MyMutex; };  

例子:

#include <iostream> #include <thread> #include <mutex>   std::mutex mut;   void Print(int num) { 	std::cout << "this is thread_unlock: " <<num<< std::endl; 	{ 		std::lock_guard<std::mutex> lg(mut);//初始化就上锁 		std::cout << "this is thread: " << num << std::endl; 	}//离开块作用域就自动解锁 }     int main() {   	std::thread t1(Print, 1); 	std::thread t2(Print, 2); 	t1.join(); 	t2.join(); 	std::cout << "this is  main thread " << std::endl; 	return 0; } 

std::unique_lock:
也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。这个比较灵活,可以让我们指定“何时”以及“如何”锁定和结果Mutex,有挺多函数给我们进行选择。
C++11的线程库

总述:
C++11的线程库

条件变量(必须先加锁)

头文件:# include < condition_variable >
std::condition_variable readyCondVar;

条件的检测是在互斥锁的保护下进行的。线程在改变条件状态之前必须首先锁住互斥量。如果条件为假,这个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量 可以被用来实现这两进程间的线程同步。

常用API接口:

C++11的线程库

代码:

std::mutex mutex; std::condition_variable cv;   // 条件变量与临界区有关,用来获取和释放一个锁,因此通常会和mutex联用。 std::unique_lock lock(mutex);      //和RALL锁机制使用 // 此处会释放lock,然后在cv上等待,直到其它线程通过cv.notify_xxx来唤醒当前线程, // cv被唤醒后会再次对lock进行上锁,然后wait函数才会返回。 // wait返回后可以安全的使用mutex保护的临界区内的数据。此时mutex仍为上锁状态 cv.wait(lock) 

(上面的wait这个函数可能会导致惊群效应,所以我们可以用重载版本,cv.wait(lock,可调用函数对象));
类似这样:

> g_cv.wait(lock, [] ){  return xxx; }); 

参考此篇文章:请点击!


notify_one()与notify_all()

点击链接查看这个知识点。(这篇博客也不严谨,我是持怀疑态度。。。)



参考文章:

  1. 博客一
版权声明:玥玥 发表于 2021-04-19 18:35:41。
转载请注明:C++11的线程库 | 女黑客导航