1. 什么是mutex?
The mutex class is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads.
C mutex
类是一个简单的同步结构,用于保护共享数据免受从多个线程同时访问,避免数据竞争,并提供线程间的同步支持。其在头文件<mutex>中定义。
mutex
类提供的方法主要有:
- lock:锁定互斥。若另一线程已锁定互斥,则到
lock
的调用将阻塞执行,直至获得锁。若lock
为已占有mutex
的线程调用,则行为未定义。 - try_lock:尝试锁定互斥。立即返回。成功获得锁时返回
true
,否则返回false
。若已占有mutex
的线程调用try_lock
,则行为未定义。 - unlock:解锁互斥。互斥必须为当前执行线程所锁定,否则行为未定义。
当在多个线程之间对共享数据进行相互独占访问,我们可以创建一个互斥对象,并使用 lock()
和 unlock()
函数使代码的共享数据一次只能用于一个线程。
#include<iostream>
#include <mutex>
std::mutex m;
m.lock();
/**********************************/
/* 这一部分代码将成为关键的一部分 */
/* 一次只有一个线程可以访问此部分 */
/**********************************/
m.unlock();
在上面的示例中,我们使用 lock
和 unlock
来包围我们的代码段,并防止它同时从多个线程访问。一旦调用 lock()
方法,只有一个线程能够访问关键部分,直到调用 unlock()
方法。因此,如果另一个线程希望进入临界部分,则必须等到第一个线程到达 unlock()
后才可以调用访问。
示例:
代码语言:javascript复制#include <chrono>
#include <mutex>
#include <thread>
#include <iostream> // std::cout
std::chrono::milliseconds interval(100);
std::mutex mutex;
int job_shared = 0; // 两个线程都能修改 'job_shared',
// mutex 将保护此变量
int job_exclusive = 0; // 只有一个线程能修改 'job_exclusive'
// 不需要保护
// 此线程能修改 'job_shared' 和 'job_exclusive'
void job_1()
{
std::this_thread::sleep_for(interval); // 令 'job_2' 持锁
while (true) {
// 尝试锁定 mutex 以修改 'job_shared'
if (mutex.try_lock()) {
std::cout << "job shared (" << job_shared << ")n";
mutex.unlock();
return;
} else {
// 不能获取锁以修改 'job_shared'
// 但有其他工作可做
job_exclusive;
std::cout << "job exclusive (" << job_exclusive << ")n";
std::this_thread::sleep_for(interval);
}
}
}
// 此线程只能修改 'job_shared'
void job_2()
{
mutex.lock();
std::this_thread::sleep_for(5 * interval);
job_shared;
mutex.unlock();
}
int main()
{
std::thread thread_1(job_1);
std::thread thread_2(job_2);
thread_1.join();
thread_2.join();
}
可能的输出:
代码语言:javascript复制job exclusive (1)
job exclusive (2)
job exclusive (3)
job exclusive (4)
job shared (1)
注:
std::mutex
既不可复制亦不可移动。
2.C 11提供的其他互斥量
mutex
提供了基本的互斥设施,在此基础上,C 11还提供了以下互斥类:
- timed_mutex:提供互斥设施,实现有时限锁定。
- recursive_mutex:提供能被同一线程递归锁定的互斥设施。
- recursive_timed_mutex:提供能被同一线程递归锁定的互斥设施,并实现有时限锁定。
timed_mutex
timed_mutex
类是能用于保护数据免受多个线程同时访问的同步原语。
其拥有类似 mutex
的行为, timed_mutex
提供排他性非递归所有权语义。另外, timed_mutex
提供通过 try_lock_for()
和 try_lock_until()
方法试图带时限地要求 timed_mutex
所有权的能力。其提供的方法主要有:
成员函数 | 作用 |
---|---|
lock | 锁定互斥,若互斥不可用则阻塞 。 |
try_lock | 尝试锁定互斥,若互斥不可用则返回 。 |
try_lock_for | 尝试锁定互斥,若互斥在指定的时限时期中不可用则返回。 |
try_lock_until | 尝试锁定互斥,若直至抵达指定时间点互斥不可用则返回 。 |
unlock | 解锁互斥。 |
try_lock_for
的函数原型如下:
//timeout_duration为要阻塞的最大时长
template< class Rep, class Period >
bool try_lock_for( const std::chrono::duration<Rep, Period>& timeout_duration ); //C 1
当前线程会在锁定成功(占有互斥量)或者经过指定的时长 timeout_duration
(超时)前阻塞,取决于何者先到来。锁定成功时返回 true
,否则返回 false
。如果timeout_duration
小于或等于 timeout_duration.zero()
,那么函数表现同try_lock()
。
注:由于调度或资源争议延迟,此函数可能阻塞长于
timeout_duration
。
try_lock_until
的函数原型如下:
//timeout_time为要阻塞到的最晚时间点
template< class Clock, class Duration >
bool try_lock_until( const std::chrono::time_point<Clock, Duration>& timeout_time ); //C 11 起
当前线程会在锁定成功(占有互斥量)或者抵达指定的时间点 timeout_time
(超时)前阻塞,取决于何者先到来。锁定成功时返回 true
,否则返回 false
。如果调用时已经过了时间点 timeout_time
,那么此函数表现同 try_lock()
。
recursive_mutex
recursive_mutex
类是同步原语,能用于保护共享数据免受从个多线程同时访问。其提供排他性递归所有权语义:
- 调用方线程在从它成功调用lock或try_lock开始的时期里占有
recursive_mutex
。此时期间,线程可以进行对lock
或try_lock
的附加调用。所有权的时期在线程调用unlock匹配次数时结束。 - 线程占有
recursive_mutex
时,若其他所有线程试图要求recursive_mutex
的所有权,则它们将阻塞(对于调用lock
)或收到false
返回值(对于调用try_lock
)。 - 可锁定
recursive_mutex
次数的最大值是未指定的,但抵达该数后,对lock
的调用将抛出std::system_error
而对try_lock
的调用将返回false
。
其提供的方法与mutex
一致,如下所示:
成员函数 | 作用 |
---|---|
lock | 锁定互斥,若互斥不可用则阻塞 。 |
try_lock | 尝试锁定互斥,若互斥不可用则返回 。 |
unlock | 解锁互斥。 |
示例:
代码语言:javascript复制#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
int g_num = 0; // 为 g_num_mutex 所保护
std::mutex g_num_mutex;
void slow_increment(int id)
{
for (int i = 0; i < 3; i) {
g_num_mutex.lock();
g_num;
std::cout << id << " => " << g_num << 'n';
g_num_mutex.unlock();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main()
{
std::thread t1(slow_increment, 0);
std::thread t2(slow_increment, 1);
t1.join();
t2.join();
}
可能的输出:
代码语言:javascript复制0 => 1
1 => 2
0 => 3
1 => 4
0 => 5
1 => 6
注:若
recursive_mutex
在仍为某线程占有时被销毁,则程序行为未定义。
recursive_timed_mutex
recursive_timed_mutex
是同步原语,能用于保护共享数据免受从多个线程同时访问。其以类似 std::recursive_mutex
的方式,提供排他性递归所有权语义。另外, recursive_timed_mutex
通过 try_lock_for
与 try_lock_until
方法,提供带时限地试图要求 recursive_timed_mutex
所有权的能力。其提供的方法如下:
成员函数 | 作用 |
---|---|
lock | 锁定互斥,若互斥不可用则阻塞 。 |
try_lock | 尝试锁定互斥,若互斥不可用则返回 。 |
try_lock_for | 尝试锁定互斥,若互斥在指定的时限时期中不可用则返回。 |
try_lock_until | 尝试锁定互斥,若直至抵达指定时间点互斥不可用则返回 。 |
unlock | 解锁互斥。 |
recursive_timed_mutex
的try_lock_for
与timed_mutex
的try_lock_for
类似,都尝试锁定互斥量。当前线程会在锁定成功(占有互斥量)或者经过指定的时长 timeout_duration
(超时)前阻塞,取决于何者先到来。锁定成功时返回 true
,否则返回 false
。
同样的,recursive_timed_mutex
的try_lock_until
与timed_mutex
的try_lock_until
类似,都尝试锁定互斥量。当前线程会在锁定成功(占有互斥量)或者抵达指定的时间点 timeout_time
(超时)前阻塞,取决于何者先到来。锁定成功时返回 true
,否则返回 false
。
3.总结
在共享资源且不希望它们同时被多个或多个线程修改的情况下我们应该使用互斥量保证我们数据的安全和有序。通过使用互斥量,我们可以锁定包含应用程序关键逻辑的对象。这也防止了数据不一致,这在实时应用程序中非常重要。同时,我们在使用lock
的时候一定要记得unlock
,否则会造成死锁,后面我们也将会继续介绍C 11中unique_lock
和lock_guard
可以避免死锁问题。