c++ 多线程 “锁”
C++11线程中的几种锁
- 互斥锁(Mutex)
- 条件锁
- 自旋锁
- 读写锁
- 递归锁
线程之间的锁有: 互斥锁、条件锁、自旋锁、读写锁、递归锁。一般而言,锁的功能与性能成反比。不过我们一般不使用递归锁(C++标准库提供了std::recursive_mutex),所以这里就不推荐了。
互斥锁(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;
}
死锁:死锁是指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局,若无外力作用,这些进程(线程)都将无法向前推进。
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;
}
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()。
条件锁
即使共享变量是原子性的,它也必须在mutex的保护下被修改,这是为了能够将改动正确发布到正在等待的线程。
任意要等待std::condition_variable的线程必须:
- 获取
std::unique_lock<std::mutex>
,这个mutex正是用来保护共享变量(即“条件”)的 - 执行wait, wait_for或者wait_until. 这些等待动作原子性地释放mutex,并使得线程的执行暂停
- 当获得条件变量的通知,或者超时,或者一个虚假的唤醒,那么线程就会被唤醒,并且获得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;
}
- 在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()。
自旋锁
假设我们有一个两个处理器core1和core2计算机,现在在这台计算机上运行的程序中有两个线程:T1和T2分别在处理器core1和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_flag
类:是一种简单的原子布尔类型,只支持两种操作:test_and_set(flag=true)和clear(flag=false)。- 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;
}
读写锁
读写锁可以解决上面的问题。它提供了比互斥锁更好的并行性。因为以读模式加锁后,当有多个线程试图再以读模式加锁时,并不会造成这些线程阻塞在等待锁的释放上。
2 如果一个线程用写锁锁住了临界区,那么其他线程无论是读锁还是写锁都会发生阻塞。
头文件:boost/thread/shared_mutex.cpp
类型:boost::shared_lock
shared_lock是read lock。被锁后仍允许其他线程执行同样被shared_lock的代码。这是一般做读操作时的需要。
递归锁
1. C++中使用的锁:mutex
假定有一个全局变量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::mutex和std::lock_guard的用法。
c++ 多线程 “锁”相关推荐
- 【JUC并发编程06】多线程锁 (公平锁和非公平锁,死锁,可重锁)
文章目录 6 多线程锁 (公平锁和非公平锁,死锁,可重锁) 6.1 synchronized 锁的八种情况 6.2 对上述例子的总结 6.3 公平锁和非公平锁 6.4 可重入锁 6.5 死锁 6 多线 ...
- java判断线程是否死锁_c++多线程锁 Mutex 自动判断死锁
c++多线程锁可以使用absl::Mutex std::mutex这两种,下面是demo代码. 使用absl:Mutex的时候打印: [mutex.cc : 1338] RAW: Cycle: [m ...
- linux下java多线程_Linux系统下Java问题排查——cpu使用率过高或多线程锁问题
原标题:Linux系统下Java问题排查--cpu使用率过高或多线程锁问题 一个系统.特别是多线程并发的后台系统,在某些特定场景下,可能触发系统中的bug:导致cpu一直居高不下.进程hang了或处理 ...
- 【JUC】第三章 多线程锁、CallableFuture 接口
第三章 多线程锁.Callable&Future 接口 文章目录 第三章 多线程锁.Callable&Future 接口 一.多线程锁 1.synchronized 2.公平锁/非公平 ...
- 多线程锁详解之【临界区】
更多的锁介绍可以先看看这篇文章:多线程锁详解之[序章] 正文: 一般锁的类型可分为两种:用户态锁和内核态锁.用户态锁是指这个锁的不能够跨进程使用.而内核态锁就是指能够跨进程使用的锁.一般书中会说,wi ...
- 锁锁锁-多线程锁-多进程锁
锁是什么 锁在现实生活意义在于通过加锁的方式达到隐私保护或者独占的意义. 锁在程序世界里,加锁是方法,目的在于①独占②同步. 多线程锁–锁的源起 1.为了尽可能压榨CPU资源,神奇的码农们发明了轻量级 ...
- 多线程锁的升级原理是什么?
多线程锁的升级原理是什么? 锁的级别从低到高: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 锁分级别原因: 没有优化以前,synchronized是重量级锁(悲观锁),使用 ...
- Java多线程 - 锁
Java多线程 - 锁 三性 可见性 指的是线程之间的可见性,一个线程对状态的修改,对其他线程是可见的.在 Java中 volatile.synchronized 和 final 实现可见性. 原子性 ...
- [java多线程] - 锁机制同步代码块信号量
在美眉图片下载demo中,我们可以看到多个线程在公用一些变量,这个时候难免会发生冲突.冲突并不可怕,可怕的是当多线程的情况下,你没法控制冲突.按照我的理解在java中实现同步的方式分为三种,分别是:同 ...
- 【多线程】多线程锁住的是什么、std::lock_guard<std::mutex> locker(mutex_)
通常不直接使用 mutex,lock_guard更加安全, 更加方便. lock_guard简化了 lock/unlock 的写法, lock_guard在构造时自动锁定互斥量, 而在退出作用域时会析 ...
最新文章
- EventBus的使用(一看就懂)
- 树莓派:django,uwsgi,nginx安装与设置
- CentOS\fedora使用yum update更新时不升级内核的方法
- C++类的组合和前向引用
- SSL与WildFly 8和Undertow
- GoogLeNet的心路历程(三)
- MYSQL绿色安装过程
- AudioTrack到AudioFlinger流程分析(三十八)
- 马哥Linux学习笔记之一——关于多磁盘的组织问题
- Cookie的格式及组成
- Python最新官方教程中文版,火了!!
- 语音识别(ASR) 阿里云
- 如何设置计算机桌面待办事项,Windows电脑桌面云便签怎么设置每天提醒待办事项?...
- Spring Security | 轻松搞定认证授权~
- oracle误删数据恢复方法
- 【Mysql面试宝典】快速搞定Mysql表操作
- django haystack一次使用总结
- eclipse项目感叹号
- 江苏省事业单位试题计算机博客,2018年1月27日江苏省省直事业单位面试题
- 正则——只能允许是汉字、拼音和数字的正则表达式
热门文章
- Python 办公自动化:全网最强最详细 PDF 文件操作手册!
- 一个工科研究生毕业后的职业规划
- 用ajax接收后台数据里的具体数据,ajax动态接收后台向后台传输数据以及接收数据...
- gcc: buildin函数: __builtin_unreachable __builtin_constant_p;__atomic_load_n
- 考试系统之选择题评分
- JAVA毕业设计口腔医院患者服务系统计算机源码+lw文档+系统+调试部署+数据库
- 北美CS求学找工指南
- jquery 身份证工具类插件
- SAP S4 HANA 1909 安装说明
- 分享一个二维码生成的接口,简单好用