C++11线程中的几种锁

  • 互斥锁(Mutex)
  • 条件锁
  • 自旋锁
  • 读写锁
  • 递归锁

线程之间的锁有: 互斥锁、条件锁、自旋锁、读写锁、递归锁。一般而言,锁的功能与性能成反比。不过我们一般不使用递归锁(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 <list>
#include <mutex>std::list<int> some_list;
std::mutex some_mutex;void add_to_list(int new_value)
{std::lock_guard<std::mutex> guard(some_mutex);some_list.push_back(new_value);
}

以下情况会出现死锁:

mutex m0,m1;
int i = 0;
void fun0()
{while (i < 100){lock_guard<mutex> g0(m0);  //线程0加锁0lock_guard<mutex> g1(m1);  //线程0加锁1cout << "thread 0 running..." << endl;}return;
}
void fun1()
{while (i < 100){lock_guard<mutex> g1(m1);  //线程1加锁1lock_guard<mutex> g0(m0);  //线程1加锁0cout << "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<mutex> g0(m0);  //线程0加锁0lock_guard<mutex> g1(m1);  //线程0加锁1cout << "thread 0 running..." << endl;}return;
}
void fun1()
{while (i < 100){lock_guard<mutex> g0(m0);  //线程1加锁0lock_guard<mutex> g1(m1);  //线程1加锁1cout << "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<mutex> g0(m0, adopt_lock);lock_guard<mutex> g1(m1, adopt_lock);cout << "thread 0 running..." << endl;}return;
}
void fun1()
{while (i < 100){lock(m0,m1);lock_guard<mutex> g0(m0, adopt_lock);lock_guard<mutex> 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_lock<std::mutex>,这个mutex正是用来保护共享变量(即“条件”)的
  2. 执行wait, wait_for或者wait_until. 这些等待动作原子性地释放mutex,并使得线程的执行暂停
  3. 当获得条件变量的通知,或者超时,或者一个虚假的唤醒,那么线程就会被唤醒,并且获得mutex. 然后线程应该检查条件是否成立,如果是虚假唤醒,就继续等待。

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

std::deque<int> q;
std::mutex mu;
std::condition_variable cond;void function_1() //生产者
{int count = 10;while (count > 0) {std::unique_lock<std::mutex> 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<std::mutex> 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;
}

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

  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 <thread>
#include <vector>
#include <iostream>
#include <atomic>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<std::thread> v;for (int n = 0; n < 10; ++n) {v.emplace_back(f, n);}for (auto& t : v) {t.join();}
}

说明:atomic是C++标准程序库中的一个头文件,定义了C++11标准中的一些表示线程、并发控制时原子操作的类与方法等。此头文件主要声明了两大类原子对象:std::atomicstd::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 <iostream>
#include <ctime>
#include <vector>
#include <thread>
#include <atomic>std::atomic<size_t> count(0);void threadFun()
{for (int i = 0; i < 10000; i++)count++;
}int main(void)
{clock_t start_time = clock();// 启动多个线程std::vector<std::thread> threads;for (int i = 0; i < 10; i++)threads.push_back(std::thread(threadFun));for (auto&thad : threads)thad.join();// 检测count是否正确 10000*10 = 100000std::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 <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>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 <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>int counter = 0;
std::mutex mtx; // 保护countervoid 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 <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>int counter = 0;
std::mutex mtx; // 保护countervoid 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 <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>int counter = 0;
std::mutex mtx; // 保护countervoid 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<std::mutex> 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()函数。这里不再给出具体的例子,如果想了解这种构造函数是如何工作的,可以看这里,链接中给的例子很简洁。

本篇主要讲述c++多线程编程中锁的基本类型和用法,主要展示了std::mutexstd::lock_guard的用法。

c++ 多线程 “锁”相关推荐

  1. 【JUC并发编程06】多线程锁 (公平锁和非公平锁,死锁,可重锁)

    文章目录 6 多线程锁 (公平锁和非公平锁,死锁,可重锁) 6.1 synchronized 锁的八种情况 6.2 对上述例子的总结 6.3 公平锁和非公平锁 6.4 可重入锁 6.5 死锁 6 多线 ...

  2. java判断线程是否死锁_c++多线程锁 Mutex  自动判断死锁

    c++多线程锁可以使用absl::Mutex  std::mutex这两种,下面是demo代码. 使用absl:Mutex的时候打印: [mutex.cc : 1338] RAW: Cycle: [m ...

  3. linux下java多线程_Linux系统下Java问题排查——cpu使用率过高或多线程锁问题

    原标题:Linux系统下Java问题排查--cpu使用率过高或多线程锁问题 一个系统.特别是多线程并发的后台系统,在某些特定场景下,可能触发系统中的bug:导致cpu一直居高不下.进程hang了或处理 ...

  4. 【JUC】第三章 多线程锁、CallableFuture 接口

    第三章 多线程锁.Callable&Future 接口 文章目录 第三章 多线程锁.Callable&Future 接口 一.多线程锁 1.synchronized 2.公平锁/非公平 ...

  5. 多线程锁详解之【临界区】

    更多的锁介绍可以先看看这篇文章:多线程锁详解之[序章] 正文: 一般锁的类型可分为两种:用户态锁和内核态锁.用户态锁是指这个锁的不能够跨进程使用.而内核态锁就是指能够跨进程使用的锁.一般书中会说,wi ...

  6. 锁锁锁-多线程锁-多进程锁

    锁是什么 锁在现实生活意义在于通过加锁的方式达到隐私保护或者独占的意义. 锁在程序世界里,加锁是方法,目的在于①独占②同步. 多线程锁–锁的源起 1.为了尽可能压榨CPU资源,神奇的码农们发明了轻量级 ...

  7. 多线程锁的升级原理是什么?

    多线程锁的升级原理是什么? 锁的级别从低到高: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 锁分级别原因: 没有优化以前,synchronized是重量级锁(悲观锁),使用 ...

  8. Java多线程 - 锁

    Java多线程 - 锁 三性 可见性 指的是线程之间的可见性,一个线程对状态的修改,对其他线程是可见的.在 Java中 volatile.synchronized 和 final 实现可见性. 原子性 ...

  9. [java多线程] - 锁机制同步代码块信号量

    在美眉图片下载demo中,我们可以看到多个线程在公用一些变量,这个时候难免会发生冲突.冲突并不可怕,可怕的是当多线程的情况下,你没法控制冲突.按照我的理解在java中实现同步的方式分为三种,分别是:同 ...

  10. 【多线程】多线程锁住的是什么、std::lock_guard<std::mutex> locker(mutex_)

    通常不直接使用 mutex,lock_guard更加安全, 更加方便. lock_guard简化了 lock/unlock 的写法, lock_guard在构造时自动锁定互斥量, 而在退出作用域时会析 ...

最新文章

  1. EventBus的使用(一看就懂)
  2. 树莓派:django,uwsgi,nginx安装与设置
  3. CentOS\fedora使用yum update更新时不升级内核的方法
  4. C++类的组合和前向引用
  5. SSL与WildFly 8和Undertow
  6. GoogLeNet的心路历程(三)
  7. MYSQL绿色安装过程
  8. AudioTrack到AudioFlinger流程分析(三十八)
  9. 马哥Linux学习笔记之一——关于多磁盘的组织问题
  10. Cookie的格式及组成
  11. Python最新官方教程中文版,火了!!
  12. 语音识别(ASR) 阿里云
  13. 如何设置计算机桌面待办事项,Windows电脑桌面云便签怎么设置每天提醒待办事项?...
  14. Spring Security | 轻松搞定认证授权~
  15. oracle误删数据恢复方法
  16. 【Mysql面试宝典】快速搞定Mysql表操作
  17. django haystack一次使用总结
  18. eclipse项目感叹号
  19. 江苏省事业单位试题计算机博客,2018年1月27日江苏省省直事业单位面试题
  20. 正则——只能允许是汉字、拼音和数字的正则表达式

热门文章

  1. Python 办公自动化:全网最强最详细 PDF 文件操作手册!
  2. 一个工科研究生毕业后的职业规划
  3. 用ajax接收后台数据里的具体数据,ajax动态接收后台向后台传输数据以及接收数据...
  4. gcc: buildin函数: __builtin_unreachable __builtin_constant_p;__atomic_load_n
  5. 考试系统之选择题评分
  6. JAVA毕业设计口腔医院患者服务系统计算机源码+lw文档+系统+调试部署+数据库
  7. 北美CS求学找工指南
  8. jquery 身份证工具类插件
  9. SAP S4 HANA 1909 安装说明
  10. 分享一个二维码生成的接口,简单好用