“不定义,做一个保持好奇心的普通人”?? ??? ? ???. 2018.12.19 快三年了: Mutex 又称互斥量,C++ 11中与 Mutex 相关的类(包括锁类型)和函数都声明在 头文件中,所以如果你需要使用 std::mutex,就必须包含 头文件。 头文件介绍Mutex 系列类(四种) std::mutex,最基本的 Mutex 类。 std::recursive_mutex,递归 Mutex 类。 std::time_mutex,定时 Mutex 类。 std::recursive_timed_mutex,定时递归 Mutex 类。 Lock 类(两种) std::lock_guard,与 Mutex RAII 相关,方便线程对互斥量上锁。 std::unique_lock,与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。 其他类型 std::once_flag std::adopt_lock_t std::defer_lock_t std::try_to_lock_t 函数 std::try_lock,尝试同时对多个互斥量上锁。 std::lock,可以同时对多个互斥量上锁。 std::call_once,如果多个线程需要同时调用某个函数,call_once 可以保证多个线程对该函数只调用一次。 std::mutex 下面以 std::mutex 为例介绍 C++11 中的互斥量用法。 std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。 std::mutex 的成员函数 构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。 lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。 unlock(), 解锁,释放对互斥量的所有权。 try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。 接下来 重点说一下lock_guard 和 unique_lock ※※std::lock_guard std::lock_gurad 是 C++11 中定义的模板类。定义如下:
template class lock_guard; 注意:无论是std::mutex还是std::lock_gurad、std::unique_lock 都是类,需要创建自己的对象使用!!! lock_guard 对象呢通常是用来管理一个 std::mutex 类型的对象,即通过定义一个 lock_guard 一个对象来管理 std::mutex 的上锁和解锁。在 lock_guard 初始化的时候进行上锁,然后在 lock_guard 析构的时候进行解锁。值得注意的是,lock_guard 对象并不负责管理 std::mutex 对象的生命周期,lock_guard 对象只是简化了 mutex 对象的上锁和解锁操作,方便线程对互斥量上锁,即在某个 lock_guard 对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 lock_guard 的生命周期结束之后,它所管理的锁对象会被解锁(注:类似 shared_ptr 等智能指针管理动态分配的内存资源 ),也就是说在使用 lock_guard 的过程中,如果 std::mutex 的对象被释放了,那么在 lock_guard 析构的时候进行解锁就会出现空指针错误之类。 在 lock_guard 对象构造时,传入的 Mutex 对象(即它所管理的 Mutex 对象)会被当前线程锁住。在lock_guard 对象被析构时,它所管理的 Mutex 对象会自动解锁,由于不需要程序员手动调用 lock 和 unlock 对 Mutex 进行上锁和解锁操作,因此这也是最简单安全的上锁和解锁方式,尤其是在程序抛出异常后先前已被上锁的 Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常处理代码。 例: // // Created by zxkj on 2018.12.19 // #include #include #include #include std::mutex mtx; using std::cout; using std::endl; void print_event(int x) { if (x % 2 == 0) { cout << x << "is event" << endl; } else { throw(std::logic_error("not event")); } } void print_id(int id) { try { std::lock_guard lck(mtx); print_event (id); } catch(std::logic_error&) { cout << "[exception caught]n"; } } int main() { std::thread threads[10]; for (int i = 0; i < 10; ++i) { threads[i] = std::thread(print_id,i+1); } for (auto &th : threads) { th.join (); } return 0; }
?※※std::unique_lockunique_lock 和 lock_guard 一样,对 std::mutex 类型的互斥量的上锁和解锁进行管理,一样也不管理 std::mutex 类型的互斥量的声明周期。但是它的使用更加的灵活。std::unique_lock 的构造函数的数目相对来说比 std::lock_guard 多,其中一方面也是因为 std::unique_lock 更加灵活,从而在构造 std::unique_lock 对象时可以接受额外的参数。总地来说,std::unique_lock 构造函数如下:(1) 默认构造函数
??? 新创建的 unique_lock 对象不管理任何 Mutex 对象。
(2) locking 初始化
??? 新创建的 unique_lock 对象管理 Mutex 对象 m,并尝试调用 m.lock() 对 Mutex 对象进行上锁,如果此时另外某个 unique_lock 对象已经管理了该 Mutex 对象 m,则当前线程将会被阻塞。
(3) try-locking 初始化
??? 新创建的 unique_lock 对象管理 Mutex 对象 m,并尝试调用 m.try_lock() 对 Mutex 对象进行上锁,但如果上锁不成功,并不会阻塞当前线程。
(4) deferred 初始化
??? 新创建的 unique_lock 对象管理 Mutex 对象 m,但是在初始化的时候并不锁住 Mutex 对象。 m 应该是一个没有当前线程锁住的 Mutex 对象。
(5) adopting 初始化
??? 新创建的 unique_lock 对象管理 Mutex 对象 m, m 应该是一个已经被当前线程锁住的 Mutex 对象。(并且当前新创建的 unique_lock 对象拥有对锁(Lock)的所有权)。
(6) locking 一段时间(duration)
??? 新创建的 unique_lock 对象管理 Mutex 对象 m,并试图通过调用 m.try_lock_for(rel_time) 来锁住 Mutex 对象一段时间(rel_time)。
(7) locking 直到某个时间点(time point)
??? 新创建的 unique_lock 对象管理 Mutex 对象m,并试图通过调用 m.try_lock_until(abs_time) 来在某个时间点(abs_time)之前锁住 Mutex 对象。
(8) 拷贝构造 [被禁用]
??? unique_lock 对象不能被拷贝构造。
(9) 移动(move)构造
??? 新创建的 unique_lock 对象获得了由 x 所管理的 Mutex 对象的所有权(包括当前 Mutex 的状态)。调用 move 构造之后, x 对象如同通过默认构造函数所创建的,就不再管理任何 Mutex 对象了。#include #include #include std::mutex foo,bar;void task_a() {std::lock(foo,bar);//foo和bar已被当前线程锁住/********************************************************adopting 初始化:*adopt_lock 是一个常量对象,通常作为参数传入给unique_lock 或*lock_guard 的构造函数。新创建的 unique_lock 对象管理 Mutex*对象 m, m 应该是一个已经被当前线程锁住的 Mutex 对象。*******************************************************/std::unique_lock lck1(foo,std::adopt_lock);std::unique_lock lck2(bar,std::adopt_lock);std::cout << "task an";}void task_b() {//新创建的 unique_lock 对象不管理任何 Mutex 对象。std::unique_lock lck1,lck2;/******************************************************* deferred 初始化:*新创建的 unique_lock 对象管理 Mutex 对象 m,但是在初始化*的时候并不锁住 Mutex 对象。 m 应该是一个没有当前线程锁住的*Mutex 对象。******************************************************/lck1 = std::unique_lock(bar,std::defer_lock);lck2 = std::unique_lock(foo,std::defer_lock);std::lock(lck1,lck2);std::cout << "task bn";}int main() {std::thread th1(task_a);std::thread th2(task_b);th1.join();th2.join();system("pause");return EXIT_SUCCESS;}?总结:??? 1. unique_lock比lock_guard使用更加灵活,功能更加强大。使用unique_lock需要付出更多的时间、性能成本。std::unique_lock也可以提供自动加锁、解锁功能??? 2. std::lock_guard 在构造函数中进行加锁,析构函数中进行解锁;是RAII模板类的简单实现,功能简单。??? 3.大部分情况下,两者的功能是一样的,不过unique_lock 比lock_guard 更灵活.unique_lock提供了lock,unlock,try_lock等接口.
lock_guard没有多余的接口,构造函数时拿到锁,析构函数时释放锁,lock_guard 比unique_lock 要省时.?? 4. lock_guard同一时间锁住两个mutex,再创建guards用来管理锁的释放工作;?????? unique_lock 先创建guards,再同时锁住两个锁。??※※std::condition_variable是C++标准程序库中的一个头文件,定义了C++11标准中的一些用于并发编程时表示条件变量的类与方法等。互斥锁std::mutex是一种最常见的线程间同步的手段,但是在有些情况下不太高效。假设想实现一个简单的消费者生产者模型,一个线程往队列中放入数据,一个线程往队列中取数据,取数据前需要判断一下队列中确实有数据,由于这个队列是线程间共享的,所以,需要使用互斥锁进行保护,一个线程在往队列添加数据的时候,另一个线程不能取,反之亦然。用互斥锁实现如下:#include #include #include #include std::deque q;std::mutex mu;void function_1() {int count = 10;while (count > 0) {std::unique_lock locker(mu);q.push_front(count);locker.unlock();std::this_thread::sleep_for(std::chrono::seconds(1));count--;}}void function_2() {int data = 0;while ( data != 1) {std::unique_lock locker(mu);if (!q.empty()) {data = q.back();q.pop_back();locker.unlock();std::cout << "t2 got a value from t1: " << data << std::endl;} else {locker.unlock();}}}int main() {std::thread t1(function_1);std::thread t2(function_2);t1.join();t2.join();return 0;}//输出结果//t2 got a value from t1: 10//t2 got a value from t1: 9//t2 got a value from t1: 8//t2 got a value from t1: 7//t2 got a value from t1: 6//t2 got a value from t1: 5//t2 got a value from t1: 4//t2 got a value from t1: 3//t2 got a value from t1: 2//t2 got a value from t1: 1可以看到,互斥锁其实可以完成这个任务,但是却存在着性能问题。首先,function_1函数是生产者,在生产过程中,std::this_thread::sleep_for(std::chrono::seconds(1));表示延时1s,所以这个生产的过程是很慢的;function_2函数是消费者,存在着一个while循环,只有在接收到表示结束的数据的时候,才会停止,每次循环内部,都是先加锁,判断队列不空,然后就取出一个数,最后解锁。所以说,在1s内,做了很多无用功!这样的话,CPU占用率会很高,可能达到100%(单核)。这就引出了条件变量(condition variable),c++11中提供了#include 头文件,其中的std::condition_variable可以和std::mutex结合一起使用,其中有两个重要的接口,notify_one()和wait(),wait()可以让线程陷入休眠状态,在消费者生产者模型中,如果生产者发现队列中没有东西,就可以让自己休眠,但是不能一直不干活啊,notify_one()就是唤醒处于wait中的其中一个条件变量(可能当时有很多条件变量都处于wait状态)。那什么时刻使用notify_one()比较好呢,当然是在生产者往队列中放数据的时候了,队列中有数据,就可以赶紧叫醒等待中的线程起来干活了。使用条件变量修改后如下:#include #include #include #include #include std::deque q;std::mutex mu;std::condition_variable cond;void function_1() {int count = 10;while (count > 0) {std::unique_lock locker(mu);q.push_front(count);locker.unlock();cond.notify_one(); // Notify one waiting thread,if there is one.std::this_thread::sleep_for(std::chrono::seconds(1));count--;}}void function_2() {int data = 0;while ( data != 1) {std::unique_lock locker(mu);while(q.empty())cond.wait(locker); // Unlock mu and wait to be notifieddata = q.back();q.pop_back();locker.unlock();std::cout << "t2 got a value from t1: " << data << std::endl;}}int main() {std::thread t1(function_1);std::thread t2(function_2);t1.join();t2.join();return 0;}上面的代码有三个注意事项:在function_2中,在判断队列是否为空的时候,使用的是while(q.empty()),而不是if(q.empty()),这是因为wait()从阻塞到返回,不一定就是由于notify_one()函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒,如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()阻塞。在管理互斥锁的时候,使用的是std::unique_lock而不是std::lock_guard,而且事实上也不能使用std::lock_guard,这需要先解释下wait()函数所做的事情。可以看到,在wait()函数之前,使用互斥锁保护了,如果wait的时候什么都没做,岂不是一直持有互斥锁?那生产者也会一直卡住,不能够将数据放入队列中了。所以,wait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作。而lock_guard没有lock和unlock接口,而unique_lock提供了。这就是必须使用unique_lock的原因。使用细粒度锁,尽量减小锁的范围,在notify_one()的时候,不需要处于互斥锁的保护范围内,所以在唤醒条件变量之前可以将锁unlock()。还可以将cond.wait(locker);换一种写法,wait()的第二个参数可以传入一个函数表示检查条件,这里使用lambda函数最为简单,如果这个函数返回的是true,wait()函数不会阻塞会直接返回,如果这个函数返回的是false,wait()函数就会阻塞着等待唤醒,如果被伪唤醒,会继续判断函数返回值。void function_2() {int data = 0;while ( data != 1) {std::unique_lock locker(mu);cond.wait(locker,[](){ return !q.empty();} ); // Unlock mu and wait to be notifieddata = q.back();q.pop_back();locker.unlock();std::cout << "t2 got a value from t1: " << data << std::endl;}}?除了notify_one()函数,c++还提供了notify_all()函数,可以同时唤醒所有处于wait状态的条件变量。?※※提供一个线程安全的堆栈实例(.hpp):#ifndef THRAEDSAFE_STACK_HPP#define THRAEDSAFE_STACK_HPP#include #include #include #include templateclass threadsafe_stack{private:mutable std::mutex mut;std::stack data_stack;std::condition_variable data_cond;public:threadsafe_stack(){}threadsafe_stack(threadsafe_stack const& other){std::lock_guard lk(other.mut);data_stack=other.data_stack;}void push(T new_value){std::lock_guard lk(mut);data_stack.push(new_value);data_cond.notify_one();}void wait_and_pop(T& value){std::unique_lock lk(mut);data_cond.wait(lk,[this]{return !data_stack.empty();});value=data_stack.top();data_queue.pop();}std::shared_ptr wait_and_pop(){std::unique_lock lk(mut);data_cond.wait(lk,[this]{return !data_stack.empty();});std::shared_ptr res(std::make_shared(data_stack.top()));data_queue.pop();return res;}bool try_pop(T& value){std::lock_guard lk(mut);if(data_stack.empty())return false;value=data_stack.top();data_stack.pop();return true;}std::shared_ptr try_pop(){std::lock_guard lk(mut);if(data_stack.empty())return std::shared_ptr();std::shared_ptr res(std::make_shared(data_stack.top()));data_stack.pop();return res;}bool empty() const{std::lock_guard lk(mut);return data_stack.empty();}};#endif?ok~未来再见! (编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|