꿈을 이루게 될 거예요.
2018.12.19
快三年了:
Mutex 又称互斥量,C++ 11中与 Mutex 相关的类(包括锁类型)和函数都声明在
Mutex 系列类(四种)
Lock 类(两种)
其他类型
函数
下面以 std::mutex 为例介绍 C++11 中的互斥量用法。
std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。
std::mutex 的成员函数
接下来
重点说一下lock_guard 和 unique_lock
std::lock_gurad 是 C++11 中定义的模板类。定义如下:
template
注意:无论是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;
}
unique_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 a\n";
}
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 b\n";
}
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::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 notified
data = 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 notified
data = q.back();
q.pop_back();
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
}
}
除了notify_one()
函数,c++
还提供了notify_all()
函数,可以同时唤醒所有处于wait
状态的条件变量。
#ifndef THRAEDSAFE_STACK_HPP
#define THRAEDSAFE_STACK_HPP
#include
#include
#include
#include
template
class 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~
未来再见!