在前面一篇文章,我们知道怎么使用线程并行地执行程序。在每个线程中执行的代码都是独立的。通常情况下,多个线程之间会用到共享数据。此时,我们就会面临一个问题:同步。
通过下面一段简单的代码来分析同步问题。
同步问题
下面这个例子,我们探讨一个简单的计数器类。这个类有一个数据成员value和成员函数(增加数据成员value)。
class Counter
{
public:
void increase()
{
value++;
}
private:
int value;
};
这并没什么新颖的东西。现在,我们启动一些线程来进行一些increase操作。
int main()
{
Counter counter(0);
std::vector<thread> threadVec;
for (int i = 0; i < 5; ++i)
{
threadVec.push_back(thread([&counter]()
{
for(int j = 0; j < 1000; ++j)
{
counter.increaseValue();
}
}
));
}
for (auto& thread : threadVec)
{
thread.join();
}
cout<<counter.value<<endl;
}
我们启动了5个线程,每个线程对counter对象进行100次的increaseValue操作。当所有的线程运行完毕,我们输出counter对象的value值。
当我们运行程序后,期待的输出结果为500.但是结果并不是这样,下面是我计算机上的输出结果:
442
500
477
400
422
487
问题在于increaseValue操作并不是一个原子操作。事实上,每一个increaseValue操作是由三步组成:
读取value的当前值
对当前值加1
将加1后的值赋给value
在单线程的情况下,上面的代码没有问题。但是在多线程环境下,上面的代码就会有问题。可以想象:
- Thread 1 : read the value, get 0, add 1, so value = 1
- Thread 2 : read the value, get 0, add 1, so value = 1
- Thread 1 : write 1 to the field value and return 1
- Thread 2 : write 1 to the field value and return 1
接下来,我们讲学习怎么用信号量去解决该问题。事实上,我们将一种特殊的信号量叫做互斥量。 互斥量是这样一个对象,同一时刻只有一个线程可以成功进行lock操作。互斥量的这个强而有力的特性可以帮助我们修正上面的同步问题。
使用信号量使我们的Counter线程安全
在C++11的线程库中,已经实现了std::mutex类,这个类有两个重要的操作mutex: lock() 和unlock()。正如他们名字,第一个函数是使线程获取锁,第二是使线程释放锁。lock方法是阻塞的,lock只有当线程获取到lock才会返回。
为了使类Counter线程安全,我们要在该类中添加成员变量set::mutex ,然后在每一个成员函数中调用 lock()/unlock() 方法。
class Counter
{
public:
Counter(int _value):value(_value){}
void increaseValue()
{
mutex.lock();
++value;
mutex.unlock();
}
int value;
std::mutex mutex;
};
我们可以测试上面的代码,输出的结果为500.和我们期望的一样。
异常和互斥量
现在,我们考虑下其它情况,Counter类有个decreaseValue操作并且当value的值为0是throw出一个异常。
现在,我们考虑下其它情况,Counter类有个decreaseValue操作并且当value的值为0是throw出一个异常。
struct Counter {
int value;
Counter() : value(0) {}
void increment(){
++value;
}
void decrement(){
if(value == 0){
throw "Value cannot be less than 0";
}
--value;
}
};
我们想在不修改此类的情况下并发地访问该类,所以我们为此类创建一个带有lock的包装器类。
struct ConcurrentCounter {
std::mutex mutex;
Counter counter;
void increment(){
mutex.lock();
counter.increment();
mutex.unlock();
}
void decrement(){
mutex.lock();
counter.decrement();
mutex.unlock();
}
};
这个包装器类在大多数情况下都没问题,但是当decreaseValue方法抛出异常时,你会碰到大问题。确实异常发生时,unlock函数不会被调用,所以这个锁也不会被释放。这样,上面代码就会被阻塞。为了修正此问题,不得不使用try/cathch结构来释放该锁。
void decrement(){
mutex.lock();
try {
counter.decrement();
} catch (std::string e){
mutex.unlock();
throw e;
}
mutex.unlock();
}
上面的代码没什么难度,但是看起来很丑陋。下面是一种很优雅的解决方法。
http://www.baptiste-wicht.com/2012/03/cp11-concurrency-tutorial-part-2-protect-shared-data/