C# · 12月 20, 2021

C++ 线程安全下Lock 类的两种使用方式

“不定义,做一个保持好奇心的普通人”

꿈을 이루게 될 거예요.

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 < 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~未来再见!