3.1 线程之间共享数据的问题
线程之间数据共享问题,都是修改数据所导致的。在编写多线程的程序时,我们应该明确一个名词——“不变量”(不变量就是一定为真的的东西。比如文中举例的,队列头一定指向队首元素或者为空。它不可能指向队列中间的某个元素。数据元素包含的指针一定指向队列中的下个元素,或者为空,而不可能指向比如说,下下个元素。然而,程序有时为了方便,可能会临时的破坏这种规定,比如说,往队列中元素A后面插入元素的时候,就需要A指向新元素,然后新元素指向A原先指向的元素,这样,在新元素指向A原先指向的元素之前,这个不变量就被破坏了,因为A不是指向队列中下一个元素。)。在编写多线程程序时,注意“不变量”的概念有时可以帮助我们避免一些问题。
3.2 用互斥元保护共享数据
3.2.1 使用C++中的互斥元
在C++中,通过构造std::mutex的实例创建互斥元,调用成员函数lock()来进行锁定它,调用成员函数unlock()来解锁它。然而,在实际使用时,我们通过手动的来锁定和解锁是一件很麻烦的事情,如:在发生异常时,我们也要在异常代码中加入解锁的操作。所以在实际应用中,我们经常使用另一个类来代替std::mutex的使用——std::lock_guard()类模板,实现了互斥元的RAII管用语法。
#include<mutex> #include<list> #include<algorithm> using namespace std; list<int> some_list; mutex some_mutex; void add_to_list(int new_val){ lock_guard<mutex> some_lock_guard(some_mutex); some_list.push_back(new_val); } void list_contains(int val_to_find){ lock_guard<mutex> some_lock_guard(some_mutex); return find(some_list.begin(), some_list.end(), val_to_find) != some_list.end(); }
使用这种方法,可以将互斥元的锁定和解锁交给局部变量来解决,如果出现异常,我们没有进行相关锁的释放,该类的析构函数会帮我们完成这些操作。
3.2.2 为保护共享数据精心组织代码
有时我们想通过将数据结构封装成支持多线程的形式,在数据结构内部进行相关操作,虽然上面的方法看似解决了并发编程的问题,其中仍然有很多隐藏的问题,如:在使用类方法时通过返回一个共享变量有关的引用或者指向共享变量有关的指针我们很可能不经意间,通过这些共享变量有关的应用或者指向共享变量有关的指针仍然可以绕过设计的数据结构来进行更改该结构中的相关共享数据。
3.2.3 发现接口中固有的竞争条件
通过枚举stack类中的size()/empty()方法来说明这两个动态变换的函数——“非不变量”来说明C++标准库中的部分函数是具有竞争条件的,在使用的时候应该注意相关细节。
3.2.4 死锁:问题和解决方案
有些代码中由于具有多个锁,在使用的时候如果在操作变量A的时候先使用锁lock_A,然后再不释放的情况下使用lock_B,而变量B则是使用顺序相反,先使用lock_B,然后使用lock_A,有时会出现A获得lock_A的同时,B获得lock_B,再继续运行时A等待B释放lock_B,B同时等待着A释放lock_A,这样两者便形成了死锁。针对这个问题C++标准库提供了一种方法,std::lock()来进行同时锁定两个或者更多的互斥元。
class some_big_obj; void swap(some_big_obj& lhs, some_big_obj& rhs); class X{ private: some_big_obj some_detial; std::mutex m; public: X(some_big_obj const& sd): some_detial(sd){} friend void swap(X& lhs, X& rhs){ if(&lhs == &rhs) return; std::lock(lhs.m, rhs.m); lock_guard<mutex>(lhs.m, std::adopt_lock); lock_guard<mutex>(rhs.m, std::adopt_lock); swap(lhs.some_detial, rhs.some_detial); } };
使用这种方法可以避免一些死锁的出现。
3.2.5 避免死锁的进一步指南