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
这些情况来自于我们的调用交错。针对这个问题,我们有以下几种解决方案:
  • Semaphores
  • Atomic references
  • Monitors
  • Condition codes
  • Compare 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/
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页