0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

c++线程中锁的基本类型和用法

科技绿洲 来源:Linux开发架构之路 作者:Linux开发架构之路 2023-11-09 15:02 次阅读

线程之间的锁有:互斥锁、条件锁、自旋锁、读写锁、递归锁。一般而言,锁的功能与性能成反比。不过我们一般不使用递归锁(C++标准库提供了std::recursive_mutex),所以这里就不推荐了。

互斥锁(Mutex)

互斥锁用于控制多个线程对他们之间共享资源互斥访问的一个信号量。也就是说是为了避免多个线程在某一时刻同时操作一个共享资源。例如线程池中的有多个空闲线程和一个任务队列。任何是一个线程都要使用互斥锁互斥访问任务队列,以避免多个线程同时访问任务队列以发生错乱。

在某一时刻,只有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞方式进行等待。

头文件:< mutex >

类型:std::mutex

用法:在C++中,通过构造std::mutex的实例创建互斥元,调用成员函数lock()来锁定它,调用unlock()来解锁,不过一般不推荐这种做法,标准C++库提供了std::lock_guard和unique_lock类模板,都是RAII风格,它们是在定义时获得锁,在析构时释放锁。它们的主要区别在于unique_lock锁机制更加灵活,可以再需要的时候进行lock或者unlock调用,不非得是析构或者构造时。std::mutex和std::lock _ guard。都声明在< mutex >头文件中。

图片

//用互斥元保护列表
#include
#include

std::list some_list;
std::mutex some_mutex;

void add_to_list(int new_value)
{
std::lock_guard guard(some_mutex);
some_list.push_back(new_value);
}

以下情况会出现死锁:

mutex m0,m1;
int i = 0;
void fun0()
{
while (i < 100)
{
lock_guard g0(m0); //线程0加锁0
lock_guard g1(m1); //线程0加锁1
cout << "thread 0 running..." << endl;
}
return;
}
void fun1()
{
while (i < 100)
{
lock_guard g1(m1); //线程1加锁1
lock_guard g0(m0); //线程1加锁0
cout << "thread 1 running... "<< i << endl;
}
return;
}
int main()
{
thread p0(fun0);
thread p1(fun1);
p0.join();
p1.join();
return 0;
}

死锁:死锁是指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局,若无外力作用,这些进程(线程)都将无法向前推进。

解决死锁的方法:

1、顺序加锁

mutex m0,m1;
int i = 0;
void fun0()
{
while (i < 100)
{
lock_guard g0(m0); //线程0加锁0
lock_guard g1(m1); //线程0加锁1
cout << "thread 0 running..." << endl;
}
return;
}
void fun1()
{
while (i < 100)
{
lock_guard g0(m0); //线程1加锁0
lock_guard g1(m1); //线程1加锁1
cout << "thread 1 running... "<< i << endl;
}
return;
}
int main()
{
thread p0(fun0);
thread p1(fun1);
p0.join();
p1.join();
return 0;
}

2、同时上锁(需要用到lock函数)++

mutex m0,m1;
int i = 0;
void fun0()
{
while (i < 100)
{
lock(m0,m1);
lock_guard g0(m0, adopt_lock);
lock_guard g1(m1, adopt_lock);
cout << "thread 0 running..." << endl;
}
return;
}
void fun1()
{
while (i < 100)
{
lock(m0,m1);
lock_guard g0(m0, adopt_lock);
lock_guard g1(m1, adopt_lock);
cout << "thread 1 running... "<< i << endl;
}
return;
}
int main()
{
thread p0(fun0);
thread p1(fun1);
p0.join();
p1.join();
return 0;
}

注意到这里的lock_guard中多了第二个参数adopt_lock,这个参数表示在调用lock_guard时,已经加锁了,防止lock_guard在对象生成时构造函数再次lock()。

条件锁

当需要死循环判断某个条件成立与否时【true or false】,我们往往需要开一个线程死循环来判断,这样非常消耗CPU。使用条件变量,可以让当前线程wait,释放CPU,如果条件改变时,我们再notify退出线程,再次进行判断。

条件锁就是所谓的条件变量,某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态。一旦条件满足以“信号量”的方式唤醒一个因为该条件而被阻塞的线程(常和互斥锁配合使用),唤醒后,需要检查变量,避免虚假唤醒。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。

头文件:< condition_variable >

类型:std::condition_variable(只和std::mutex一起工作) 和 std::condition_variable_any(符合类似互斥元的最低标准的任何东西一起工作)。

图片

C++标准库在< condition_variable >中提供了条件变量,借由它,一个线程可以唤醒一个或多个其他等待中的线程。

想要修改共享变量(即“条件”)的线程必须:

  1. 获得一个std::mutex
  2. 当持有锁的时候,执行修改动作
  3. 对std::condition_variable执行notify_one或notify_all(当做notify动作时,不必持有锁)

即使共享变量是原子性的,它也必须在mutex的保护下被修改,这是为了能够将改动正确发布到正在等待的线程。

任意要等待std::condition_variable的线程必须:

  1. 获取std::unique_lockstd::mutex,这个mutex正是用来保护共享变量(即“条件”)的
  2. 执行wait, wait_for或者wait_until. 这些等待动作原子性地释放mutex,并使得线程的执行暂停
  3. 当获得条件变量的通知,或者超时,或者一个虚假的唤醒,那么线程就会被唤醒,并且获得mutex. 然后线程应该检查条件是否成立,如果是虚假唤醒,就继续等待。

【注:所谓虚假唤醒,就是因为某种未知的罕见的原因,线程被从等待状态唤醒了,但其实共享变量(即条件)并未变为true。因此此时应继续等待】

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;
}

上面的代码有三个注意事项:

1.在function_2中,在判断队列是否为空的时候,使用的是while(q.empty()),而不是if(q.empty()),这是因为wait()从阻塞到返回,不一定就是由于notify_one()函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒。如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()阻塞;

2.在管理互斥锁的时候,使用的是std::unique_lock而不是std::lock_guard, 而且事实上也不能使用std::lock_guard。这需要先解释下wait()函数所做的事情,可以看到,在wait()函数之前,使用互斥锁保护了,如果wait的时候什么都没做,岂不是一直持有互斥锁?那生产者也会一直卡住,不能够将数据放入队列中了。所以,wait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作。lock_guard没有lock和unlock接口,而unique_lock提供了,这就是必须使用unique_lock的原因;

3.使用细粒度锁,尽量减小锁的范围,在notify_one()的时候,不需要处于互斥锁的保护范围内,所以在唤醒条件变量之前可以将锁unlock()。

图片

自旋锁

假设我们有一个两个处理器core1和core2计算机,现在在这台计算机上运行的程序中有两个线程:T1和T2分别在处理器core1和core2上运行,两个线程之间共享着一个资源。

首先我们说明互斥锁的工作原理,互斥锁是是一种sleep-waiting的锁。假设线程T1获取互斥锁并且正在core1上运行时,此时线程T2也想要获取互斥锁(pthread_mutex_lock),但是由于T1正在使用互斥锁使得T2被阻塞。当T2处于阻塞状态时,T2被放入到等待队列中去,处理器core2会去处理其他任务而不必一直等待(忙等)。也就是说处理器不会因为线程阻塞而空闲着,它去处理其他事务去了。

而自旋锁就不同了,自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2肯定得不到这个自旋锁。与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止。

从“自旋锁”的名字也可以看出来,如果一个线程想要获取一个被使用的自旋锁,那么它会一致占用CPU请求这个自旋锁使得CPU不能去做其他的事情,直到获取这个锁为止,这就是“自旋”的含义。

当发生阻塞时,互斥锁可以让CPU去处理其他的任务;而自旋锁让CPU一直不断循环请求获取这个锁。通过两个含义的对比可以我们知道“自旋锁”是比较耗费CPU的。

// 用户空间用 atomic_flag 实现自旋互斥
#include
#include
#include
#include

std::atomic_flag lock = ATOMIC_FLAG_INIT;

void f(int n)
{
for (int cnt = 0; cnt < 100; ++cnt) {
while (lock.test_and_set(std::memory_order_acquire)) // 获得锁
; // 自旋
std::cout << "Output from thread " << n << 'n';
lock.clear(std::memory_order_release); // 释放锁
}
}

int main()
{
std::vector v;
for (int n = 0; n < 10; ++n) {
v.emplace_back(f, n);
}
for (auto& t : v) {
t.join();
}
}

说明:atomic是C++标准程序库中的一个头文件,定义了C++11标准中的一些表示线程、并发控制时原子操作的类与方法等。此头文件主要声明了两大类原子对象:std::atomic和std::atomic_flag。

1、atomic_flag类:是一种简单的原子布尔类型,只支持两种操作:test_and_set(flag=true)和clear(flag=false)。

2、std::atomic类模板:std::atomic既不可复制亦不可移动。atomic对int、char、bool等数据结构进行了原子性封装,在多线程环境中,对std::atomic对象的访问不会造成竞争-冒险。利用std::atomic可实现数据结构的无锁设计。

所谓的原子操作,取的就是“原子是最小的、不可分割的最小个体”的意义,它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。使用原子操作能大大的提高程序的运行效率。

#include
#include
#include
#include
#include


std::atomic count(0);

void threadFun()
{
for (int i = 0; i < 10000; i++)
count++;
}

int main(void)
{
clock_t start_time = clock();

// 启动多个线程
std::vector threads;
for (int i = 0; i < 10; i++)
threads.push_back(std::thread(threadFun));
for (auto&thad : threads)
thad.join();

// 检测count是否正确 10000*10 = 100000
std::cout << "count number:" << count << std::endl;

clock_t end_time = clock();
std::cout << "耗时:" << end_time - start_time << "ms" << std::endl;

return 0;
}

读写锁

先看看互斥锁,它只有两个状态,要么是加锁状态,要么是不加锁状态。假如现在一个线程a只是想读一个共享变量 i,因为不确定是否会有线程去写它,所以我们还是要对它进行加锁。但是这时又有一个线程b试图去读共享变量 i,发现被锁定了,那么b不得不等到a释放了锁后才能获得锁并读取 i 的值,但是两个读取操作即使是同时发生的,也并不会像写操作那样造成竞争,因为它们不修改变量的值。所以我们期望在多个线程试图读取共享变量的时候,它们可以立刻获取因为读而加的锁,而不是需要等待前一个线程释放。

读写锁可以解决上面的问题。它提供了比互斥锁更好的并行性。因为以读模式加锁后,当有多个线程试图再以读模式加锁时,并不会造成这些线程阻塞在等待锁的释放上。

读写锁是多线程同步的另外一个机制。在一些程序中存在读操作和写操作问题,对某些资源的访问会存在两种可能情况,一种情况是访问必须是排他的,就是独占的意思,这种操作称作写操作,另外一种情况是访问方式是可以共享的,就是可以有多个线程同时去访问某个资源,这种操作称为读操作。这个问题模型是从对文件的读写操作中引申出来的。把对资源的访问细分为读和写两种操作模式,这样可以大大增加并发效率。读写锁比互斥锁适用性更高,并行性也更高。

需要注意的是,这里只是说并行效率比互斥高,并不是速度一定比互斥锁快,读写锁更复杂,系统开销更大。并发性好对于用户体验非常重要,假设互斥锁需要0.5秒,使用读写锁需要0.8秒,在类似学生管理系统的软件中,可能90%的操作都是查询操作。如果突然有20个查询请求,使用的是互斥锁,则最后的查询请求被满足需要10秒,估计没人接收。使用读写锁时,因为读锁能多次获得,所以20个请求中,每个请求都能在1秒左右被满足,用户体验好的多。

读写锁特点

1 如果一个线程用读锁锁定了临界区,那么其他线程也可以用读锁来进入临界区,这样可以有多个线程并行操作。这个时候如果再用写锁加锁就会发生阻塞。写锁请求阻塞后,后面继续有读锁来请求时,这些后来的读锁都将会被阻塞。这样避免读锁长期占有资源,防止写锁饥饿。

2 如果一个线程用写锁锁住了临界区,那么其他线程无论是读锁还是写锁都会发生阻塞。

头文件:boost/thread/shared_mutex.cpp

类型:boost::shared_lock

用法:你可以使用boost::shared_ mutex的实例来实现同步,而不是使用std::mutex的实例。对于更新操作,std::lock_guard< boost::shared _mutex>和 std::unique _lock< boost::shared _mutex>可用于锁定,以取代相应的std::mutex特化。这确保了独占访问,就像std::mutex那样。那些不需要更新数据结构的线程能够转而使用 boost::shared _lock< boost::shared _mutex>来获得共享访问。这与std::unique _lock用起来正是相同的,除了多个线程在同一时间,同一boost::shared _mutex上可能会具有共享锁。唯一的限制是,如果任意一个线程拥有一个共享锁,试图获取独占锁的线程会被阻塞,知道其他线程全都撤回它们的锁。同样的,如果一个线程具有独占锁,其他线程都不能获取共享锁或独占锁,直到第一个线程撤回它的锁。

简单的说:

shared_lock是read lock。被锁后仍允许其他线程执行同样被shared_lock的代码。这是一般做读操作时的需要。

unique_lock是write lock。被锁后不允许其他线程执行被shared_lock或unique_lock的代码。在写操作时,一般用这个,可以同时限制unique_lock的写和share_lock的读。

递归锁

std::recursive_mutex 与 std::mutex 一样,也是一种可以被上锁的对象,但是和 std::mutex 不同的是,std::recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

例如函数a需要获取锁mutex,函数b也需要获取锁mutex,同时函数a中还会调用函数b。如果使用std::mutex必然会造成死锁。但是使用std::recursive_mutex就可以解决这个问题。

1. C++中使用的锁:mutex

锁,是生活中应用十分广泛的一种工具。锁的本质属性是为事物提供“访问保护”,例如:大门上的锁,是为了保护房子免于不速之客的到访;自行车的锁,是为了保护自行车只有owner才可以使用;保险柜上的锁,是为了保护里面的合同和金钱等重要东西……

在c++等高级编程语言中,锁也是用来提供“访问保护”的,不过被保护的东西不再是房子、自行车、金钱,而是内存中的各种变量。此外,计算机领域对于“锁”有个响亮的名字——mutex(互斥量),学过操作系统的同学对这个名字肯定很熟悉。

Mutex,互斥量,就是互斥访问的量。这种东东只在多线程编程中起作用,在单线程程序中是没有什么用处的。从c++11开始,c++提供了std::mutex类型,对于多线程的加锁操作提供了很好的支持。下面看一个简单的例子,对于mutex形成一个直观的认识。

Demo1——无锁的情况

假定有一个全局变量counter,启动两个线程,每个都对该变量自增10000次,最后输出该变量的值。在第一个demo中,我们不加锁,代码文件保存为:mutex_demo1_no_mutex.cpp

#include
#include
#include
#include
#include
#include

int counter = 0;
void increase(int time) {
for (int i = 0; i < time; i++) {
// 当前线程休眠1毫秒
std::this_thread::sleep_for(std::chrono::milliseconds(1));
counter++;
}
}

int main(int argc, char** argv) {
std::thread t1(increase, 10000);
std::thread t2(increase, 10000);
t1.join();
t2.join();
std::cout << "counter:" << counter << std::endl;
return 0;
}

为了显示多线程竞争导致结果不正确的现象,在每次自增操作的时候都让当前线程休眠1毫秒

对应 CMakeLists.txt

# 声明要求的 cmake 最低版本
cmake_minimum_required(VERSION 3.0.0)
# 声明一个 cmake 工程
project(HelloMutex)
# 设置编译模式
set(CMAKE_BUILD_TYPE "Debug")
# 语法:add_executable( 程序名 源代码文件 )
add_executable(${PROJECT_NAME} mutex_demo1_no_mutex.cpp)

if(WIN32)
set(PLATFROM_LIBS Ws2_32 mswsock iphlpapi ntdll)
else(WIN32)
set(PLATFROM_LIBS pthread ${CAMKE_DL_LIBS})
endif(WIN32)
# 将库文件链接到可执行程序上
target_link_libraries(${PROJECT_NAME} ${PLATFROM_LIBS})

如果没有多线程编程的相关经验,我们可能想当然的认为最后的counter为20000,如果这样想的话,那就大错特错了。下面是两次实际运行的结果:

[root@2d129aac5cc5 demo]# ./mutex_demo1_no_mutex
counter:19997
[root@2d129aac5cc5 demo]# ./mutex_demo1_no_mutex
counter:19996

出现上述情况的原因是:自增操作"counter++"不是原子操作,而是由多条汇编指令完成的。多个线程对同一个变量进行读写操作就会出现不可预期的操作。以上面的demo1作为例子:假定counter当前值为10,线程1读取到了10,线程2也读取到了10,分别执行自增操作,线程1和线程2分别将自增的结果写回counter,不管写入的顺序如何,counter都会是11,但是线程1和线程2分别执行了一次自增操作,我们期望的结果是12!!!!!

轮到mutex上场。

Demo2——加锁的情况

定义一个std::mutex对象用于保护counter变量。对于任意一个线程,如果想访问counter,首先要进行"加锁"操作,如果加锁成功,则进行counter的读写,读写操作完成后释放锁(重要!!!);如果“加锁”不成功,则线程阻塞,直到加锁成功。

#include
#include
#include
#include
#include
#include

int counter = 0;
std::mutex mtx; // 保护counter

void increase(int time) {
for (int i = 0; i < time; i++) {
mtx.lock();
// 当前线程休眠1毫秒
std::this_thread::sleep_for(std::chrono::milliseconds(1));
counter++;
mtx.unlock();
}
}

int main(int argc, char** argv) {
std::thread t1(increase, 10000);
std::thread t2(increase, 10000);
t1.join();
t2.join();
std::cout << "counter:" << counter << std::endl;
return 0;
}

上述代码保存文件为:mutex_demo2_with_mutex.cpp。先来看几次运行结果:

[root@2d129aac5cc5 demo]# ./mutex_demo2_with_mutex
counter:20000
[root@2d129aac5cc5 demo]# ./mutex_demo2_with_mutex
counter:20000
[root@2d129aac5cc5 demo]# ./mutex_demo2_with_mutex
counter:20000

这次运行结果和我们预想的一致,原因就是“利用锁来保护共享变量”,在这里共享变量就是counter(多个线程都能对其进行访问,所以就是共享变量啦)。

简单总结一些std::mutex:

  • 对于std::mutex对象,任意时刻最多允许一个线程对其进行上锁
  • mtx.lock():调用该函数的线程尝试加锁。如果上锁不成功,即:其它线程已经上锁且未释放,则当前线程block。如果上锁成功,则执行后面的操作,操作完成后要调用mtx.unlock()释放锁,否则会导致死锁的产生
  • mtx.unlock():释放锁
  • std::mutex还有一个操作:mtx.try_lock(),字面意思就是:“尝试上锁”,与mtx.lock()的不同点在于:如果上锁不成功,当前线程不阻塞。

2. lock_guard

虽然std::mutex可以对多线程编程中的共享变量提供保护,但是直接使用std::mutex的情况并不多。因为仅使用std::mutex有时候会发生死锁。回到上边的例子,考虑这样一个情况:假设线程1上锁成功,线程2上锁等待。但是线程1上锁成功后,抛出异常并退出,没有来得及释放锁,导致线程2“永久的等待下去”(线程2:我的心在等待永远在等待……),此时就发生了死锁。给一个发生死锁的 :

Demo3——死锁的情况(仅仅为了演示,不要这么写代码哦)

为了捕捉抛出的异常,我们重新组织一下代码,代码保存为:mutex_demo3_dead_lock.cpp。

#include
#include
#include
#include
#include
#include

int counter = 0;
std::mutex mtx; // 保护counter

void increase_proxy(int time, int id) {
for (int i = 0; i < time; i++) {
mtx.lock();
// 线程1上锁成功后,抛出异常:未释放锁
if (id == 1) {
throw std::runtime_error("throw excption....");
}
// 当前线程休眠1毫秒
std::this_thread::sleep_for(std::chrono::milliseconds(1));
counter++;
mtx.unlock();
}
}

void increase(int time, int id) {
try {
increase_proxy(time, id);
}
catch (const std::exception& e){
std::cout << "id:" << id << ", " << e.what() << std::endl;
}
}

int main(int argc, char** argv) {
std::thread t1(increase, 10000, 1);
std::thread t2(increase, 10000, 2);
t1.join();
t2.join();
std::cout << "counter:" << counter << std::endl;
return 0;
}

执行后,结果如下图所示:

[root@2d129aac5cc5 demo]# ./mutex_demo3_dead_lock
id:1, throw excption....

程序并没有退出,而是永远的“卡”在那里了,也就是发生了死锁。

那么这种情况该怎么避免呢?这个时候就需要std::lock_guard登场了。std::lock_guard只有构造函数和析构函数。简单的来说:当调用构造函数时,会自动调用传入的对象的lock()函数,而当调用析构函数时,自动调用unlock()函数(这就是所谓的RAII,读者可自行搜索)。我们修改一下demo3。

Demo4——避免死锁,lock_guard

demo4保存为:mutex_demo4_lock_guard.cpp

#include
#include
#include
#include
#include
#include

int counter = 0;
std::mutex mtx; // 保护counter

void increase_proxy(int time, int id) {
for (int i = 0; i < time; i++) {
// std::lock_guard对象构造时,自动调用mtx.lock()进行上锁
// std::lock_guard对象析构时,自动调用mtx.unlock()释放锁
std::lock_guard lk(mtx);
// 线程1上锁成功后,抛出异常:未释放锁
if (id == 1) {
throw std::runtime_error("throw excption....");
}
// 当前线程休眠1毫秒
std::this_thread::sleep_for(std::chrono::milliseconds(1));
counter++;
}
}

void increase(int time, int id) {
try {
increase_proxy(time, id);
}
catch (const std::exception& e){
std::cout << "id:" << id << ", " << e.what() << std::endl;
}
}

int main(int argc, char** argv) {
std::thread t1(increase, 10000, 1);
std::thread t2(increase, 10000, 2);
t1.join();
t2.join();
std::cout << "counter:" << counter << std::endl;
return 0;
}

执行上述代码,结果为:

[root@2d129aac5cc5 demo]# ./mutex_demo4_lock_guard
id:1, throw excption....
counter:10000

结果符合预期。所以,推荐使用std::mutex和std::lock_guard搭配使用,避免死锁的发生。

3. std::lock_guard的第二个构造函数

实际上,std::lock_guard有两个构造函数,具体的(参考:cppreference):

explicit lock_guard( mutex_type& m ); (1) (since C++11)
lock_guard( mutex_type& m, std::adopt_lock_t t ); (2) (since C++11)
lock_guard( const lock_guard& ) = delete; (3) (since C++11)

在demo4中我们使用了第1个构造函数,第3个为拷贝构造函数,定义为删除函数。这里我们来重点说一下第2个构造函数。

第2个构造函数有两个参数,其中第二个参数类型为:std::adopt_lock_t。这个构造函数假定:当前线程已经上锁成功,所以不再调用lock()函数,这里不再给出具体的例子。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 参数
    +关注

    关注

    11

    文章

    1398

    浏览量

    31472
  • 函数
    +关注

    关注

    3

    文章

    3882

    浏览量

    61310
  • C++
    C++
    +关注

    关注

    21

    文章

    2066

    浏览量

    72900
  • 线程
    +关注

    关注

    0

    文章

    489

    浏览量

    19495
收藏 人收藏

    评论

    相关推荐

    c++冒号(:)和双冒号(::)的用法

    ()——————————————————————————————————————VC如下::是C++里的“作用域分解运算符”。比如声明了一个类A,类A里声明了一个成员函数voidf(),但没有在类的声明里给出f的定义,那么在类外定义f时,就要写成voidA::f
    发表于 10-18 10:08

    CC++const的用法比较

    为0)。针对Cconst的上述局限性,C++作出了重大的改进。在C++,可以使用const来定义常数,因为const在编译器的控制范畴内
    发表于 11-11 10:00

    C语言中数据类型介绍(基本类型、构造类型、指针类型、空类型

    C语言数据类型包括:基本类型、构造类型、指针类型、空类型; ==》空
    发表于 07-19 09:09

    Vulkan的基本类型

    Vulkan基本类型
    发表于 02-19 07:08

    C++的四种类型转换分别是哪些?C++析构函数的作用是什么

    C++的四种类型转换分别是哪些?C++析构函数的作用是什么?在C语言中关键字static主要
    发表于 12-24 06:57

    C语言的数据基本类型分为哪几种

    今天闲着无聊把书翻看一遍,收获颇丰。目录1、数据类型本类型数据长度强制类型转换bit强制类型转换2、变量类型局部变量全局变量变量的存储类别
    发表于 02-25 06:35

    C语言数据的基本类型

    现在大家只需要学习一下基本类型。其他三种类型更适合在后续分享相关知识点时详细介绍。
    的头像 发表于 05-05 20:08 3265次阅读

    Vulkan API 基本类型介绍

    Vulkan 基本类型,Vulkan 开发需要设计的类型非常多,整理其基本类型如下,主要包含设备、队列、命令缓冲、队列家族、渲染通,管线等……
    的头像 发表于 02-12 16:19 1508次阅读

    Vulkan API 基本类型 小结

    Vulkan 基本类型,Vulkan 开发需要设计的类型非常多,整理其基本类型如下,主要包含设备、队列、命令缓冲、队列家族、渲染通,管线等……
    发表于 02-23 06:02 5次下载
    Vulkan API 基<b class='flag-5'>本类型</b> 小结

    浅析巴伦的功能原理、性能参数、基本类型

    浅析巴伦的功能原理、性能参数、基本类型
    发表于 01-21 09:52 10次下载

    C++的引用和指针

    之前的文章我们已经介绍了C++中的基本类型如int,bool和double等,除了基本类型C++还有一些更复杂的数据类型复合
    的头像 发表于 03-17 14:00 431次阅读

    如何用C++实现一个线程池呢?

    C++线程池是一种多线程管理模型,把线程分成任务执行和线程调度两部分。
    发表于 06-08 14:53 798次阅读
    如何用<b class='flag-5'>C++</b>实现一个<b class='flag-5'>线程</b>池呢?

    运动误差基本类型原因与对策

    运动误差基本类型原因与对策
    发表于 06-20 15:58 0次下载

    线程之间的锁的基本类型用法

    线程之间的锁有:互斥锁、条件锁、自旋锁、读写锁、递归锁。一般而言,锁的功能与性能成反比。不过我们一般不使用递归锁(C++标准库提供了std::recursive_mutex),所以这里就不
    的头像 发表于 11-09 09:15 253次阅读
    <b class='flag-5'>线程</b>之间的锁的基<b class='flag-5'>本类型</b>和<b class='flag-5'>用法</b>

    javascript基本类型有哪些

    JavaScript中的基本类型包括: 数字类型(Number):表示数字,包括整数和浮点数。 字符串类型(String):表示文本数据。 布尔类型(Boolean):表示逻辑值,即真
    的头像 发表于 11-16 10:32 235次阅读