Mutex全名mutual exclusio,用来协助采取独占排他方式控制对资源的并发访问。这里的资源指的是计算机中的内存,硬件等。为了实现独占资源,必须对相应的线程锁定mutex,这样可以防止其他线程也锁定mutex,知道锁定mutex的线程对mutex解除锁定(unlock)
在使用mutex/lock的时候,应该使用RAII守则即资源获取即初始化,在构造函数中获取资源,而在析构函数中释放资源。C++提供了std::lock_guard来帮助完成对mutex的自动加锁,以及自动解锁。也就是在这一线程任务完成的时候实现自动解锁。不使用std::lock_guard的例子如下:
1 | std::mutex m; |
如果在func中发生异常,有可能导致无法unlock,从而导致死锁。而使用lock_guard则不会出现由于func发生异常而导致死锁。使用lock_guard因为异常而自动析构并解锁。使用std::lock_guard的示例代码如下:
1 | std::mutex m; |
这样的lock应该限制在最短周期内,因为它们会阻塞其他代码的运行机会。
递归的lock(Recursive lock)
有的时候递归锁定也是必要的,最典型的就是monitor,它们在每个public函数内放一个mutex并取得其lock,以防止data race。但是如果使用mutex锁上之后,再次使用mutex如果使用的顺序不当有可能引发死锁,但是使用recursive_mutex,则可以避免一些问题。这个mutex允许同一线程多次锁定,并在最近一次相应的unlock中释放mutex。代码示例如下:
1 | class DataBaseAccess{ |
try_lock 和 timed_mutex
当程序想要获得一个lock但是如果不可能成功的话它不想阻塞,针对 这种情况,mutex成员函数提供try_lock()函数,它试图获取一个lock,成功就返回true,否则返回false,为了能够使用lock_guard(使作用域的任何出口都可以自动解锁),可以额外提供一个自适应的锁给构造函数,示例代码如下:
1 | std::mutex m; |
为了等待特定长度的时间,可以使用timed_mutex。c++提供了std::timed_mutex和std::recursive_timed_mutex允许使用try_lock_for()或者try_lock_until(),用来等待某个时间段,或者到达某个时间点。示例代码如下:
1 | std::timed_mutex m; |
当然对于某些情况可以获取多个锁i,主要是为了传送数据,从一个受保护资源到另外一个。可以直接使用是std::lock()函数进行锁定,但是有可能导致死锁,或者有可能只取到第一个lock而无法取到第二个lock。std::lock()函数会锁定它收到的所有的mutex,而且阻塞直到所有的mutex被锁或发生异常。成功锁定之后应该使用lock_guard并指定第二参数std::adopt_lock,保证在退出作用域时都能被解锁。如下:
1 | std::mutex m1; |
也可以使用try_lock()函数,尝试获取多个锁,如果能够取到所有lock的话,该函数会返回-1,否则返回第一个失败的lock索引(从0开始),如果全部成功的话,这些锁会被解锁。如下:
1 | std::mutex m1; |
unique_lock
unique_lock对于mutex更有弹性,接口与lock_guard的接口一样,但又允许明确写出何时以及如何锁定或解锁mutex,对于unique_lock可以调用owns_lock()或bool()来查询其mutex是否被锁定,如果析构时mutex被锁定,其析构函数会自动调用unlock(),如果mutex没有被锁定,则析构函数什么也不做。示例如下:
1 | //试图获取锁,但不阻塞 |