有时任务间需要共享数据。此时数据访问必须进行同步,以确保在同一时刻至多有一个任务能访问数据。有经验的程序员可能认为这是一种简单化的方法(例如,很多任务同时读取不变的数据是没有任何问题的),但无论如何,确保在同一时刻至多有一个任务可以访问给定的对象是很有意义的.
解决此问题的基础是“互斥对象” mutex。thread使用lock()操作来获取一个互斥对象;
mutex m; //控制共享数据访问的mutexint sh; //共享的数据void f(){unique_lock<mutex> lck{m}; //获取mutexsh+=7; //处理共享数据//隐式释放mutex}
unique_lock的构造函数获取了互斥对象(通过调用 m.lock()。如果另一个线程已经获取了互斥对象,则当前线程会等待(“阻塞”)直至那个线程完成对共享数据的访问。一旦线程完成了对共享数据的访问, unique_lock会释放 mutex(通过调用 m.unlock()。互斥和锁机制在头文件<mutex>中提供。
共享对象和 mutex间是一种常规的对应关系:程序员只需知道哪个 mutex对应哪个数据即可。显然这很容易出错,我们最好努力借助多种语言特性来使这样的对应关系更为清晰。例如:
class Record{public:mutex rm;//...};
对于一个名为rec的 Record,不难猜测rec.rm是一个 mutex,在访问rec的其他数据前应该先获取这个互斥对象。可见,通过注释或好的命名方式可以提高程序的可读性。
需要同时访问多个资源来执行一个操作的情况并不罕见,这可能导致死锁。例如,如果thread1获取了mutex1然后试图获取mutex2,而同时 thread2已经获取了 mutex2然后试图获取 mutex1,则两个任务都无法继续执行了。标准库提供了一个同时获取多个锁的操作可以帮助解决这个问题:
void f(){//...unique_lock<mutex> lck1{m1,defer_lock} //推迟加锁:还未尝试获取mutexunique_lock<mutex> lck2{m2,defer_lock}unique_lock<mutex> lck3{m3,defer_lock}//...lock(lck1,lck2,lck3);//处理共享数据}//隐式释放所有mutex
lock()调用只有在获取了全部 mutex实参后才会继续执行,当它持有 mutex时,绝不会阻塞(“睡眠”),当然也就不会导致死锁。unique_lock的析构函数保证了当thread离开作用域时mutex会被释放。
通过共享数据进行通信是一种很底层的方式。特别是,程序员必须想方设法了解不同的任务已经做了哪些工作,又有哪些工作尚未完成。在这方面,使用共享数据不如调用一返回模式。另一方面,有些人深信数据共享肯定比参数拷贝和结果返回更高效。如果处理大量数据,这种观点可能确实是对的,但同时加锁和解锁也是代价相当高的操作。而且,现代计算机拷贝数据的效率已经很高,特别是紧凑的数据,如 vector的元素。因此,不要为了所谓“效率”就不经思考、不经测试地选择使用共享数据的方式来进行线程间通信.
