C++多线程编程(上):https://developer.aliyun.com/article/1508301
lock类
std::lock_gurad 是 C++11 中定义的模板类。
lock_guard 对象通常用于管理某个锁(Lock)对象,因此与 Mutex RAII 相关,方便线程对互斥量上锁,即在某个 lock_guard 对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 lock_guard 的生命周期结束之后,它所管理的锁对象会被解锁(注:类似 shared_ptr 等智能指针管理动态分配的内存资源)。
在 lock_guard 对象构造时,传入的 Mutex 对象会被当前线程锁住。在lock_guard 对象被析构时,它所管理的 Mutex 对象会自动解锁,即不需要程序员手动调用 lock 和 unlock 对 Mutex 进行上锁和解锁操作,这种情况下在程序抛出异常后先前已被上锁的 Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常处理代码。
值得注意的是,lock_guard 对象并不负责管理 Mutex 对象的生命周期,lock_guard 对象只是简化了 Mutex 对象的上锁和解锁操作:即在某个 lock_guard 对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 lock_guard 的生命周期结束之后,它所管理的锁对象会被解锁。(一般是以{}来作为一个生命周期)
例如以下代码:
// 参考https://blog.csdn.net/coolwriter/article/details/79884298 // example 1-4 #include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::lock_guard #include <stdexcept> // std::logic_error std::mutex mtx; void print_even(int x) { if (x % 2 == 0) std::cout << x << " is even\n"; else throw (std::logic_error("not even")); } void print_thread_id(int id) { try { // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception: std::lock_guard<std::mutex> lck(mtx); print_even(id); } catch (std::logic_error&) { std::cout << "[exception caught]\n"; } } int main() { std::thread threads[10]; // spawn 10 threads: for (int i = 0; i<10; ++i) threads[i] = std::thread(print_thread_id, i + 1); for (auto& th : threads) th.join(); system("pause"); return 0; }
最后的输出为:
[exception caught] 2 is even [exception caught] 4 is even [exception caught] 6 is even [exception caught] 8 is even [exception caught] 10 is even 请按任意键继续. . .
综上所述:lock_guard 最大的特点就是安全易于使用,在异常抛出的时候通过 lock_guard 对象管理的 Mutex 可以得到正确地解锁。但它的缺点也很明显:那就是过于简单,不具有灵活性。
针对这一缺点,C++11有如下新特性:unique_lock
unique_lock 对象以独占所有权的方式管理 mutex 对象的上锁和解锁操作,所谓独占所有权,即该mutex 对象的所有权只能由该unique_lock占有。
在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。
std::unique_lock 对象也能保证在其自身析构时它所管理的 Mutex 对象能够被正确地解锁(即使没有显式地调用 unlock 函数)。因此,和 lock_guard 一样,这也是一种简单而又安全的上锁和解锁方式,尤其是在程序抛出异常后先前已被上锁的 Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常处理代码。
值得注意的是,unique_lock 对象同样也不负责管理 Mutex 对象的生命周期,unique_lock 对象只是简化了 Mutex 对象的上锁和解锁操作。unique_lock 的生命周期结束之后,它所管理的锁对象会被解锁,这一点和 lock_guard 类似,但 unique_lock 给程序员提供了更多的自由。
四、条件变量condition_variable
条件变量(Condtion Variable)是在多线程程序中用来实现**“等待->唤醒”逻辑常用的方法。**
举个简单的例子,应用程序A中包含两个线程t1和t2。t1需要在bool变量test_cond为true时才能继续执行,而test_cond的值是由t2来改变的,这种情况下,如何来写程序呢?可供选择的方案有两种:
第一种是t1定时的去轮询变量test_cond,如果test_cond为false,则继续休眠;如果test_cond为true,则开始执行。
第二种就是上面提到的条件变量,t1在test_cond为false时调用cond_wait进行等待,t2在改变test_cond的值后,调用cond_signal,唤醒在等待中的t1,告诉t1 test_cond的值变了,这样t1便可继续往下执行。
很明显,上面两种方案中,第二种方案是比较优的。在第一种方案中,在每次轮询时,如果t1休眠的时间比较短,会导致cpu资源浪费很厉害;如果t1休眠的时间比较长,又会导致应用逻辑处理不够及时,致使应用程序性能下降。第二种方案就是为了解决轮询的弊端而生的。(参考自https://blog.csdn.net/erickhuang1989/article/details/8754357)
请看如下例子:
//example 1-7 #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock #include <condition_variable> // std::condition_variable #include<iostream> std::mutex mtx; // 全局互斥锁. std::condition_variable cv; // 全局条件变量. bool ready = false; // 全局标志位. void do_print_id(int id) { std::unique_lock <std::mutex> lck(mtx); while (!ready) // 如果标志位不为 true, 则等待... cv.wait(lck); // 当前线程被阻塞, 当全局标志位变为 true 之后, // 线程被唤醒, 继续往下执行打印线程编号id. std::cout << "thread " << id << '\n'; } void go() { std ::unique_lock <std::mutex> lck(mtx); ready = true; // 设置全局标志位为 true. cv.notify_all(); // 唤醒所有线程. } int main() { std::thread threads[10]; // spawn 10 threads: for (int i = 0; i < 10; ++i) threads[i] = std::thread(do_print_id, i); std::cout << "10 threads ready to race...\n"; go(); // go! for (auto & th : threads) th.join(); system("pause"); return 0; }
结果为:
10 threads ready to race... thread 9 thread 6 thread 8 thread 5 thread 7 thread 2 thread 1 thread 4 thread 3 thread 0 请按任意键继续. . .
代码中判断是否ready的时候,使用的是while(),而不是 if(q.empty()),这是因为wait()从阻塞到返回,不一定就是由于notify_one()函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒,如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()阻塞。
std::condition_variable提供了两种 wait() 函数。一是当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁(mutex),不妨设获得锁 lck),直到另外某个线程调用 notify_* 唤醒了当前线程。
在线程被阻塞时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_ 唤醒了当前线程)*,wait()函数也是自动调用 lck.lock(),使得lck的状态和 wait 函数被调用时相同。
在第二种情况下(即设置了 Predicate),**只有当 pred 条件为false 时调用 wait() 才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred 为 true 时才会被解除阻塞。**请注意以下代码跟之前的区别:
#include <thread> // std::thread, std::this_thread::yield #include <mutex> // std::mutex, std::unique_lock #include <condition_variable> // std::condition_variable #include <iostream> std::mutex mtx; std::condition_variable cv; int cargo = 0; bool shipment_available() { return cargo != 0; } // 消费者线程. void consume(int n) { for (int i = 0; i < n; ++i) { std::unique_lock <std::mutex> lck(mtx); cv.wait(lck, shipment_available); std::cout << cargo << '\n'; cargo = 0; } } int main() { std::thread consumer_thread(consume, 10); // 消费者线程. // 主线程为生产者线程, 生产 10 个物品. for (int i = 0; i < 10; ++i) { while (shipment_available()) std::this_thread::yield();//当前线程“放弃”执行(在一个时间拍片的时间内),让操作系统调度另一线程继续执行,也就是主线程放弃执行,让子线程先进行 std::unique_lock <std::mutex> lck(mtx); cargo = i + 1; cv.notify_one(); } consumer_thread.join(); system("pause"); return 0; }
结果为:
1 2 3 4 5 6 7 8 9 10 请按任意键继续. . .
与std::condition_variable::wait() 类似,**不过 wait_for可以指定一个时间段,在当前线程收到通知或者指定的时间 rel_time 超时之前,该线程都会处于阻塞状态。**而一旦超时或者收到了其他线程的通知,wait_for返回,剩下的处理步骤和 wait()类似。
另外,wait_for 的重载版本的最后一个参数pred表示 wait_for的预测条件,只有当 pred条件为false时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred为 true时才会被解除阻塞。
详见:https://www.2cto.com/kf/201506/411327.html
值得注意的是:
在C++的condition_variable中,当 std::condition_variable对象的某个wait函数被调用的时候,它使用 std::unique_lock(通过 std::mutex) 来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒当前线程。
std::condition_variable 对象通常使用 std::unique_lock 来等待,如果需要使用另外的 lockable 类型,可以使用std::condition_variable_any类。
这里需要注意,在管理互斥锁的时候,使用的是std::unique_lock而不是std::lock_guard,而且事实上也不能使用std::lock_guard,这需要先解释下wait()函数所做的事情。
可以看到,在wait()函数之前,使用互斥锁保护了,如果wait的时候什么都没做?那生产者也会一直卡住,不能够将数据放入队列中了。所以,wait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面变量。而lock_guard没有lock和unlock接口,而unique_lock提供了。这就是必须使用unique_lock的原因。
有关这个部分的进一步解释请参考:
https://blog.csdn.net/shichao1470/article/details/89856443
https://www.cnblogs.com/lidabo/p/11423825.html
五、异步调用future
在上文的代码中基本都用到的thread对象,它是C++11中提供同步创建多线程的工具。但是我们想要从线程中返回异步任务结果,一般需要依靠全局变量;从安全角度看,有些不妥;为此C++11提供了std::future类模板,future对象提供访问异步操作结果的机制,很轻松解决从异步任务中返回结果。
创建方法如下:
auto futureFunction = std::async(helloFunction, “function”); //参考博客:https://blog.csdn.net/y396397735/article/details/82381874
跟线程的创建一样:一个可调用的对象以及可选的入口参数;
运行futureFunction.get()即可获取该调用对象的返回值;
这个时候需要了解两种策略:Eager or lazy evaluation(急速或惰性求值) 是计算表达式结果的两种策略。
在急速求值的情况下,将立即计算评估表达式。
在惰性求值的情况下,只会在需要的情况下评估表达式。
通常惰性求值被称为call-by-need,按需调用。
值得注意的是,通常情况下, std::async 立即开始执行它的工作包。
C++运行时会决定, 计算是发生在同一个线程还是一个新的线程。
使用std::launch::async参数的话,std::async 将在一个新线程中运行它。
相反,使用参数 std::launch::deferred, std::async将在同一个线程中运行它的工作包,这属于惰性求值。
这意味着,急速求值是立即执行的,惰性求值的策略std::launch::deferred是随着future调用get()后才开始执行。
参考如下代码:
//example 1-8 #include <future> #include <iostream> #include<string> using namespace std; bool is_prime(int x) { for (int i = 1; i<x; i++) { if (x % i == 0) return false; } return true; } int main() { std::future<bool> fut = std::async(is_prime, 700020007); std::cout << "please wait"; std::chrono::milliseconds span(1); while (fut.wait_for(span) != std::future_status::ready) std::cout << "."; std::cout << std::endl; bool ret = fut.get(); std::cout << "final result: " << std::to_string(ret) << std::endl; system("pause"); return 0; }
std::async会首先创建线程执行is_prime(700020007), 任务创建之后,std::async立即返回一个std::future对象。
主线程既可使用std::future::get获取结果,如果调用过程中,任务尚未完成,则主线程阻塞至任务完成。
主线程也可使用std::future::wait_for等待结果返回,wait_for可设置超时时间,如果在超时时间之内任务完成,则返回std::future_status::ready状态;如果在超时时间之内任务尚未完成,则返回std::future_status::timeout状态。(参考https://www.cnblogs.com/taiyang-li/p/5914167.html)
Std::promise
std::promise是C++11并发编程中常用的一个类,常配合std::future使用。其作用是在一个线程t1中保存一个类型typename T的值,可供相绑定的std::future对象在另一线程t2中获取。
Std::launch是枚举类型,用于启动异步任务时,传递给函数async的参数,它的定义如下:
enum class launch { async = 0x1,创建线程的时候就开始调用(创建新线程) deferred = 0x2延迟调用,等到wait或者get的时候才进行调用(直接在原有的线程进行); };
补充:async与thread的区别:关于系统资源紧张与否、返回值获取的难易程度;thread必定会创建线程,而这可能会造成系统崩溃;
六、atomic
std::atomic为C++11封装的原子数据类型。
从功能上看,简单地说,原子数据类型不会发生数据竞争,能直接用在多线程中而不必我们用户对其进行添加互斥资源锁的类型。从实现上,大家可以理解为这些原子类型内部自己加了锁。
atomic的变量支持–,++,+=等对本体的操作,但是类似于=+1的操作就会出错(相当于调用了两次);
std::atomic_flag
std::atomic_flag是一个原子的布尔类型,可支持两种原子操作:
test_and_set, 如果atomic_flag对象被设置,则返回true; 如果atomic_flag对象未被设置,则设置之,返回false
clear, 清楚atomic_flag对象
std::atomic_flag可用于多线程之间的同步操作,类似于linux中的信号量。使用atomic_flag可实现mutex,请参考如下代码.
//来自https://www.cnblogs.com/taiyang-li/p/5914331.html #include <iostream> #include <atomic> #include <vector> #include <thread> #include <sstream> std::atomic_flag lock = ATOMIC_FLAG_INIT; std::stringstream stream; void append_numer(int x) { while (lock.test_and_set()); stream << "thread#" << x << "\n"; lock.clear(); } int main() { std::vector<std::thread> ths; for (int i=0; i<10; i++) ths.push_back(std::thread(append_numer, i)); for (int i=0; i<10; i++) ths[i].join(); std::cout << stream.str(); return 0; }
std::atomic对int, char, bool等数据结构进行原子性封装,在多线程环境中,对std::atomic对象的访问不会造成竞争-冒险。利用std::atomic可实现数据结构的无锁设计。如下列代码:
//来自https://www.cnblogs.com/taiyang-li/p/5914331.html #include <iostream> #include <atomic> #include <vector> #include <thread> #include <sstream> std::atomic<bool> ready(false); std::atomic_flag winner = ATOMIC_FLAG_INIT; void count1m(int i) { while (!ready); for (int i=0; i<1000000; i++); if (!winner.test_and_set()) std::cout << "winner: " << i << std::endl; } int main() { std::vector<std::thread> ths; for (int i=0; i<10; i++) ths.push_back(std::thread(count1m, i)); ready = true; for (int i=0; i<10; i++) ths[i].join(); return 0; }