互斥量
谈及C++11多线程里面的互斥量,首先浮现在脑海的是互斥量是有什么用,怎么用?下面简短介绍一下互斥量,有什么用?用来解决多线程对共享数据的访问(读与写)机制,简单说就是实现多个线程间对资源的互斥访问。怎么用?C++11里面的互斥量相关的使用方法比较多样化,我们本章先学习mutex的方式。
代码演示
我们先看下面这段没有加锁的问题代码:
我们将下面代码的①②③④取消注释,加上锁后程序就正常运行。这部分代码简单说明了多线程对数据的读与取时候需要加锁。订票线程与出票线程函数,问题主要在于假设某一时刻订票线程正准备添加一个票,还未结束。这个时刻另外一个线程需要进行出票,这样会造成数据的竞争。导致程序崩溃。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
class Tickets
{
public:
// 订票线程
void book_tickets()
{
for (int i = 0; i < 1000000; i++)
{
std::cout << "订票开始,该线程执行:" << i << std::endl;
//mtx_tickets_.lock(); // ①
tickets_.push_back(i);
//mtx_tickets_.unlock(); // ②
}
std::cout << "订票线程执行完毕." << std::endl;
}
// 出票线程
void issue_tickets()
{
for (int i = 0; i < 1000000; i++)
{
if (!tickets_.empty()) // 订票量不为空
{
//mtx_tickets_.lock(); // ③
int ticket = tickets_.front();
tickets_.pop_front();
//mtx_tickets_.unlock(); // ④
}
else
{
std::cout << "订票队列里面为空:" << i << std::endl;
}
}
std::cout << "出票线程执行完毕." << std::endl;
}
private:
std::list<int> tickets_;
std::mutex mtx_tickets_;
};
int main(void)
{
Tickets tickets;
std::thread bookTickets(&Tickets::book_tickets, &tickets);
std::thread issueTickets(&Tickets::issue_tickets, &tickets);
bookTickets.join();
issueTickets.join();
std::cout << "主线程执行结束." << std::endl;
system("pause");
return 0;
}
上面程序,我们简单的使用mutex进行演示互斥锁的用途:
- 使用
std::mutex
声明一个类对象,调用lock()与unlock()对线程进行上锁,避免多个线程产生竞争机制。 - 调用类成员函数的线程使用。
std::thread bookTickets(&Tickets::book_tickets, &tickets);
死锁
我们知道多个线程对共享数据进行访问时候添加互斥量会有效的避免数据竞争的问题,但是也会出现死锁的情况。死锁:线程卡死,无法继续有效执行。举例:看下面示意图。
上图中展示死锁的关键错误处,主要在于不同互斥量上锁的顺序不一致导致的死锁现象。下面我们简单说下死锁的流程:
book_tickets
函数线程执行,mtx_tickets_0互斥量对该线程上锁,抢到执行资源,但是此时CPU进行上下文切换…issue_tickets
函数线程开始执行,mtx_tickets_1互斥量对该线程上锁,抢到继续执行的资源…- 这个时候,mtx_tickets_0互斥量对订票线程函数继续向下执行,需要继续上锁mts_tickets_1,无法继续执行因为取票线程的mtx_tickets_1还未解锁,同时mtx_tickets_1互斥量对取票线程函数继续向下执行,需要加锁mtx_tickets_0,同样无法继续执行因为订票线程的mtx_tickets_0还未进行解锁。如此,就会导致线程卡死在这里。
通过有序的对互斥量上锁来解决这个问题。下面为错误代码的示例,解决下面代码的问题,只需要将①③的顺序一致即可。或者我们可以直接使用std::lock()函数。并且std::lock(mtx_tickets_0, mtx_tickets_1)里面的互斥量对象的顺序不需要对齐。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
class Tickets
{
public:
// 订票线程
void book_tickets()
{
for (int i = 0; i < 1000000; i++)
{
std::cout << "订票开始,该线程执行:" << i << std::endl;
mtx_tickets_0.lock(); // ①
mtx_tickets_1.lock(); // ①
tickets_.push_back(i);
mtx_tickets_0.unlock(); // ②
mtx_tickets_1.unlock(); // ②
}
std::cout << "订票线程执行完毕." << std::endl;
}
// 出票线程
void issue_tickets()
{
for (int i = 0; i < 1000000; i++)
{
if (!tickets_.empty()) // 订票量不为空
{
mtx_tickets_1.lock(); ③
mtx_tickets_0.lock(); ③
int ticket = tickets_.front();
tickets_.pop_front();
mtx_tickets_1.unlock(); // ④
mtx_tickets_0.unlock(); // ④
}
else
{
std::cout << "订票队列里面为空:" << i << std::endl;
}
}
std::cout << "出票线程执行完毕." << std::endl;
}
private:
std::list<int> tickets_;
std::mutex mtx_tickets_0;
std::mutex mtx_tickets_1;
};
int main(void)
{
Tickets tickets;
std::thread bookTickets(&Tickets::book_tickets, &tickets);
std::thread issueTickets(&Tickets::issue_tickets, &tickets);
bookTickets.join();
issueTickets.join();
std::cout << "主线程执行结束." << std::endl;
system("pause");
return 0;
}
小结
互斥量为多线程进行数据访问竞争提供一种有效的解决办法。但是使用互斥量mutex时候需要注意一下问题:
- 使用mutex时候,先lock()锁住数据,操作共享的数据,然后unlock()解锁。
- 记住lock()加锁与unlock()解锁一定要成对使用,这里的成对使用是lock()加锁之后,千万记得unlock()。特别会在条件判断过程中,unlock()解锁一定要保证任何流程执行下来都与之前的lock()对应。
- 为了避免使用mutex时候发生死锁,建议上锁时候互斥量的顺序一致。
- 如果多个线程对共享数据都只读的话,那么不需要使用mutex;但是,如果存在读与写,那么一定使用互斥量进行操作,避免错误发生。同理
- 关于使用mutex进行加锁lock()与解锁unlock()的位置也是需要仔细考虑的,例如上面的程序,如果你把订票线程加锁lock()与解锁unlock()在for循环外面,出票线程,那么整个程序的效率将会降低(这里我们几乎忽略上下文切换的耗时)。
- 避免死锁产生,建议对上锁的互斥量顺序尽量保持一致。或者直接使用
std::lock()
函数来对多个互斥量对象上锁出进行操作。
上面总结了mutex的一些使用策略与避坑方法,当然lock()与unlock()成对使用难免会忘记,C++11多线程还提供了lock_guard类上锁机制来降低这个mutex的代码写错的可能性。但是,mutex的灵活性更高,控制上锁的区域更加灵活。lock_guard就稍微逊色些mutex。
参考
https://zh.cppreference.com/w/cpp/thread/mutex
https://www.jianshu.com/p/ce782ac3150b