线程同步的基本概念
线程同步不是一起、相同,而是协调、协同的意思。
-
按预定的先后次序进行运行,线程A生成数据后交给线程B处理;
-
公共资源同一时刻只能被一个线程使用;共享数据在同一时刻只能被一个线程修改,以保证数据的完整性。
包括的内容有“互斥锁、条件变量、信号量、自旋锁、读写锁
一、互斥锁
头文件#include
std::mutex:
声明:mutex mtx;
1.对于std::mutex对象,任意时刻最多允许一个线程对其进行上锁
2.mtx.lock():调用该函数的线程尝试加锁。如果上锁不成功,即:其它线程已经上锁且未释放,则当前线程block。如果上锁成功,则执行后面的操作,操作完成后要调用mtx.unlock()释放锁,否则会导致死锁的产生。
-
mtx.unlock():释放锁
-
std::mutex还有一个操作:mtx.try_lock(),字面意思就是:“尝试上锁”,与mtx.lock()的不同点在于:如果上锁不成功,当前线程不阻塞。
pthread_mutex_t mutex; //声明互斥锁
pthread_mutex_init(&mutex,NULL); //初始化锁
pthread_mutex_lock(&mutex); //阻塞加锁
pthread_mutex_trylock(&mutex);//非阻塞加锁
pthread_mutex_unlock(&mutex);//解锁
pthread_mutex_destroy(&mutex); //销毁锁,释放资源
二、lock_guard
虽然std::mutex可以对多线程编程中的共享变量提供保护,但是直接使用std::mutex的情况并不多。因为仅使用std::mutex有时候会发生死锁。回到上边的例子,考虑这样一个情况:假设线程1上锁成功,线程2上锁等待。但是线程1上锁成功后,抛出异常并退出,没有来得及释放锁,导致线程2“永久的等待下去”(线程2:我的心在等待永远在等待……),此时就发生了死锁。给一个发生死锁的 :
死锁的情况:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>
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,读者可自行搜索)。我们修改一下demo:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>
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<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搭配使用,避免死锁的发生。
std::lock_guard的第二个构造函数
第二个构造函数有两个参数,其中第二个参数类型为:std::adopt_lock_t。这个构造函数假定:当前线程已经上锁成功(必须要把互斥量提前lock,否则会报异常)
三、unique_lock
unique_lock相比较lock_guard更灵活,但效率低,因为要维护锁的状态,内存占用更多。
3.1 unique_lock的第二个参数
1. std::adopt_lock
与lock_guard的作用一样,需要提前上锁。
2.try_to_lock
我们会尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,我也会立即返回,并不会阻塞在那里;
3.defer_lock
如果没有第二个参数就对mutex进行加锁,加上defer_lock是始化了一个没有加锁的mutex;
不给它加锁的目的是以后可以调用unique_lock的一些方法;
前提:不能提前lock()。
3.2 unique_lock的成员函数
3.3 unique_lock的成员函数(前三个与defer_lock联合使用)
3.3.1 lock():加锁
unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
3.3.2 unlock():解锁
void inMsgRecvQueue(){
for (int i = 0; i < 10000; i++){
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
std::unique_lock<std::mutex> sbguard(my_mutex, std::defer_lock);//没有加锁的my_mutex
sbguard.lock();//咱们不用自己unlock
//处理共享代码
//因为有一些非共享代码要处理
sbguard.unlock();
//处理非共享代码要处理。。。
sbguard.lock();
//处理共享代码
msgRecvQueue.push_back(i);
//...
//其他处理代码
sbguard.unlock();//画蛇添足,但也可以
}
}
因为一些非共享代码要处理,可以暂时先unlock(),用其他线程把它们处理了,处理完后再lock()。
3.3.3 try_lock():尝试给互斥量加锁
如果拿不到锁,返回false,否则返回true。
void inMsgRecvQueue(){
for (int i = 0; i < 10000; i++)
{
std::unique_lock<std::mutex> sbguard(my_mutex, std::defer_lock);//没有加锁的my_mutex
if (sbguard.try_lock() == true)//返回true表示拿到锁了
{
msgRecvQueue.push_back(i);
//...
//其他处理代码
}
else
{
//没拿到锁
cout << "inMsgRecvQueue()执行,但没拿到锁头,只能干点别的事" << i << endl;
}
}
}
lock的代码段越少,执行越快,整个程序的运行效率越高。
a.锁住的代码少,叫做粒度细,执行效率高;
b.锁住的代码多,叫做粒度粗,执行效率低;
四、复制与转移
lock_guard不能复制也不能转移,unique_lock可以转移,不可以复制。
// unique_lock 可以移动,不能复制
std::unique_lock<std::mutex> guard1(_mu);
std::unique_lock<std::mutex> guard2 = guard1; // error
std::unique_lock<std::mutex> guard2 = std::move(guard1); // ok
// lock_guard 不能移动,不能复制
std::lock_guard<std::mutex> guard1(_mu);
std::lock_guard<std::mutex> guard2 = guard1; // error
std::lock_guard<std::mutex> guard2 = std::move(guard1); // error
原文链接: https://www.cnblogs.com/cuijy1/p/16225100.html
欢迎关注
微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍
原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/189016
非原创文章文中已经注明原地址,如有侵权,联系删除
关注公众号【高性能架构探索】,第一时间获取最新文章
转载文章受原作者版权保护。转载请注明原作者出处!