1. 竞争条件
并发代码中最常见的错误之一就是竞争条件
(race condition)。而其中最常见的就是数据竞争
(data race),从整体上来看,所有线程之间共享数据的问题,都是修改数据导致的,如果所有的共享数据都是只读的,就不会发生问题。
而c++中常见的cout就是一个共享资源
,如果在多个线程同时执行cout,你会发发现很奇怪的问题:
#include <iostream>
#include <thread>
#include <string>
using namespace std;
// 普通函数 无参
void function_1() {
for(int i=0; i>-100; i--)
cout << "From t1: " << i << endl;
}
int main()
{
std::thread t1(function_1);
for(int i=0; i<100; i++)
cout << "From main: " << i << endl;
t1.join();
return 0;
}
你有很大的几率发现打印会出现类似于From t1: From main: 64
这样奇怪的打印结果。cout是基于流的,会先将你要打印的内容放入缓冲区,可能刚刚一个线程刚刚放入From t1:,另一个线程就执行了,导致输出变乱
。而c语言中的printf不会发生这个问题
。
2. 锁定(lock)与解锁(unlock)
解决办法就是要对cout这个共享资源进行保护。在c++中,可以使用互斥锁std::mutex
进行资源保护,头文件是#include <mutex>
,共有两种操作:锁定(lock)与解锁(unlock)
。将cout重新封装成一个线程安全的函数:
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
using namespace std;
std::mutex mu;
// 使用锁保护
void shared_print(string msg, int id) {
mu.lock(); // 上锁
cout << msg << id << endl;
mu.unlock(); // 解锁
}
void function_1() {
for(int i=0; i>-100; i--)
shared_print(string("From t1: "), i);
}
int main()
{
std::thread t1(function_1);
for(int i=0; i<100; i++)
shared_print(string("From main: "), i);
t1.join();
return 0;
}
注意std::mutex mu;
将锁设置为全局变量。
修改完之后,运行可以发现打印没有问题了。
3. std::lock_guard 类模板
再看上述程序,发现还有一个隐藏着的问题:如果mu.lock()和mu.unlock()之间的语句发生了异常
,会发生什么?unlock()语句没有机会执行
!
这会导致mu一直处于锁着的状态,其他使用shared_print()函数的线程就会阻塞
。
解决这个问题也很简单,c++库已经提供了std::lock_guard
类模板,构造时自动加锁,析构时自动解锁
,上面的例子可以这样改:
void shared_print(string msg, int id) {
//构造的时候帮忙上锁,析构的时候释放锁
std::lock_guard<std::mutex> guard(mu);
//mu.lock(); // 上锁
cout << msg << id << endl;
//mu.unlock(); // 解锁
}
可以实现自己的std::lock_guard
,类似这样:
class MutexLockGuard
{
public:
explicit MutexLockGuard(std::mutex& mutex)
: mutex_(mutex)
{
mutex_.lock();
}
~MutexLockGuard()
{
mutex_.unlock();
}
private:
std::mutex& mutex_;
};
上述解决的问题称之为死锁:如果你将某个mutex上锁了,却一直不释放,另一个线程访问该锁保护的资源的时候,就会发生死锁。
4. std::unique_lock 类模板
》》互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作
,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域
,也就是使用细粒度锁。
》》这一点lock_guard
做的不好,不够灵活,lock_guard
只能保证在析构的时候执行解锁操作,lock_guard
本身并没有提供加锁和解锁的接口,但是有些时候会有这种需求。看下面的例子。
class LogFile {
std::mutex _mu;
ofstream f;
public:
LogFile() { f.open("log.txt"); }
~LogFile() { f.close(); }
void shared_print(string msg, int id) {
{
std::lock_guard<std::mutex> guard(_mu);
//do something 1
}
//do something 2
//...
{
std::lock_guard<std::mutex> guard(_mu);
// do something 3
f << msg << id << endl;
cout << msg << id << endl;
}
}
};
》》上面的代码中,一个函数内部有两段代码需要进行保护,这个时候使用lock_guard
就需要创建两个局部对象
来管理同一个互斥锁(其实也可以只创建一个,但是锁的力度太大,效率不行),修改方法是使用unique_lock
。它提供了lock()和unlock()接口
,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁(lock_guard就一定会解锁)。上面的代码修改如下:
class LogFile {
std::mutex _mu;
ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void shared_print(string msg, int id) {
std::unique_lock<std::mutex> guard(_mu);
//do something 1
guard.unlock(); //临时解锁
//do something 2
guard.lock(); //继续上锁
// do something 3
f << msg << id << endl;
cout << msg << id << endl;
// 结束时析构guard会临时解锁
// 这句话可要可不要,不写,析构的时候也会自动执行
// guard.ulock();
}
};
转载自 c++多线程编程