C++11 并发教程——Part2:保护共享数据

在前面一篇文章,我们知道怎么使用线程并行地执行程序。在每个线程中执行的代码都是独立的。通常情况下,多个线程之间会用到共享数据。此时,我们就会面临一个问题:同步。


通过下面一段简单的代码来分析同步问题。

同步问题

下面这个例子,我们探讨一个简单的计数器类。这个类有一个数据成员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

在单线程的情况下,上面的代码没有问题。但是在多线程环境下,上面的代码就会有问题。可以想象:
  1. Thread 1 : read the value, get 0, add 1, so value = 1
  2. Thread 2 : read the value, get 0, add 1, so value = 1
  3. Thread 1 : write 1 to the field value and return 1
  4. Thread 2 : write 1 to the field value and return 1
这些情况来自于我们的调用交错。针对这个问题,我们有以下几种解决方案:
SemaphoresAtomic references
MonitorsCondition codesCompare and swap
接下来,我们讲学习怎么用信号量去解决该问题。事实上,我们将一种特殊的信号量叫做互斥量。 互斥量是这样一个对象,同一时刻只有一个线程可以成功进行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出一个异常。
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/

猜你喜欢

转载自blog.csdn.net/wangyangkobe/article/details/13362729
今日推荐