文章大图来源: pixiv_id=114658725
1 unique_lock 简介
1.1 基本概念
std::unique_lock
是 C++ 标准库中的一个类模板,定义在 <mutex>
头文件中。用于管理互斥量(Mutex)的加锁与解锁操作,并且提供了一种灵活的 RAII (资源获取即初始化)机制来更好地处理多线程并发同步问题。
1.2 对比 lock_guard
相比于 std::lock_guard
,std::unique_lock
更加灵活。默认情况下, std::unique_lock
和 std::lock_guard
是一样的,会自动地进行加锁和解锁。
除此以外,std::unique_lock
允许在对象的生命周期内手动对互斥量的加锁和解锁。可以在定义 std::unique_lock
对象稍后的某个时间点显式地调用 lock()
(需要配合 std::defer_lock
);可以在之后的某个时间点显式地调用 unlock()
,用于提前或者暂时解锁。而 std::lock_guard
是无法执行这些操作的。
但是,由于 std::unique_lock
支持了复杂的操作,相对于 std::lock_guard
来说 开销 是 比较大 的。因此,对于比较简单的自动加锁解锁的情形,我们尽量优先使用 std::lock_guard
;对于比较复杂的,需要支持暂时解锁、重新加锁等手动操作的情形,我们可以考虑使用 std::unque_lock
。
2 unique_lock 构造
2.1 构造 unique_lock 对象
假如当前有一个线程执行 foo()
函数,有一个共享变量 count
和一个 std::mutex
互斥量 mtx
,在对共享数据进行访问和修改前,构造一个 unique_lock 对象,尝试对互斥量 mtx
加锁。如果当前可以获得锁,那么进行加锁,然后访问和修改数据,最后进行自动解锁;如果当前无法获得锁(被其他线程占用),那么当前线程会被阻塞,直到可以获得锁。
例如下面这种形式:
1 2 3 4 5 6 7
| std::mutex mtx; int count;
void foo(){ std::unique_lock<std::mutex> lck(mtx); count ++ ; }
|
以下是一个简单的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <iostream> #include <thread> #include <mutex> #include <vector>
std::mutex mtx; int count;
void foo(){ std::unique_lock<std::mutex> lck(mtx); count ++ ; std::cout << "thread id = " << std::this_thread::get_id(); std::cout << ", count = " << count << "\n"; }
int main(){ std::vector<std::thread> v(10); for(auto& t : v) t = std::thread(foo); for(auto& t : v) t.join();
return 0; }
|
在上面的代码中创建了 10 个子线程(用 std::vector
存储),每个线程执行 foo()
函数,对共享变量 count
进行 +1
,并输出线程 id 以及当前线程对数据修改操作后数据 count
的值。
定义了一个互斥量 mtx
,并采用 std::unique_lock
实现自动加锁解锁,保证多线程互斥访问和修改共享数据。
可能的运行结果如下:

2.2 unique_lock::lock() 和 unique_lock::unlock()
std::unique_lock
也有自己的成员函数 lock()
和 unlock()
。分别用于在某个时间点手动地加锁和解锁。
如果需要 暂时解锁,或者需要 提前解锁,可以手动调用 std::unique_lock 成员函数 unlock()
解锁。有时我们线程函数中的一些操作是不需要锁保护的,而且能够 使得其他线程在此期间有机会获得锁,手动暂时解锁这时就显得很有用了。
之后如果要 再进行加锁,可以直接调用 std::unique_lock 成员函数 lock()
手动加锁。
例如下面这种形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| std::mutex mtx; int count;
void foo(){ std::unique_lock<std::mutex> lck(mtx); count ++ ; lck.unlock();
do_something_else();
lck.lock(); count ++ ; }
|
以下是一个简单的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| #include <iostream> #include <thread> #include <mutex> #include <vector> #include <chrono>
std::mutex mtx; int count;
void foo(){ std::unique_lock<std::mutex> lck(mtx); count ++ ; std::cout << "thread id = " << std::this_thread::get_id(); std::cout << ", count = " << count << "\n";
lck.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
lck.lock(); count ++ ; std::cout << "thread id = " << std::this_thread::get_id(); std::cout << ", count = " << count << "\n"; }
int main(){ std::vector<std::thread> v(10); for(auto& t : v) t = std::thread(foo); for(auto& t : v) t.join();
std::cout << "final value = " << count << "\n";
return 0; }
|
在上面的代码的 foo()
中,先定义一个 std::unique_lock
实现自动尝试加锁,如果获得了锁,那么对共享数据 count
进行 +1
修改操作,并输出当前线程 id 和 count
的值。
然后我们需要做一些其他不需要锁保护的操作,此时调用 std::unique_lock::unlock()
进行手动解锁(其他不需要锁保护的操作用 std::this_thread::sleep_for()
线程休息操作进行了模拟)。
执行完其他不需要锁保护的操作时,我们要再一次对共享数据 count
进行 +1
修改操作,因此需要在之前需要调用 std::unique_lock::lock()
再重新加锁,确保线程互斥访问和修改数据。
最后函数作用域结束 return 时自动地解锁。线程函数执行完毕。
可能的运行结果如下:

可以看出,创建的 10 个线程两次数据访问和 +1
修改操作中间暂时解锁休息的一段时间中,有其他线程获得了锁并执行自己的数据数据访问和 +1
修改操作,这表明暂时解锁使得其他线程有机会获得了锁,提高了运行的效率。
最终运行的结果为 20
,这个结果是正确的,表明这 10 个线程互斥地对共享数据进行了两次 +1
的操作。
2.3 std::defer_lock
std::defer_lock
是一个标记类型,用于 延迟加锁。我们可以在构造 std::unique_lock
时加上第二个参数 std::defer_lock
来支持延迟加锁。例如我们可以在定义完 std::unique_lock
对象之后,先进行一些其他操作,然后再调用 std::unique_lock
的成员函数 lock()
进行手动加锁。
例如下面这种形式:
1 2 3 4 5 6 7 8 9 10 11
| std::mutex mtx; int count;
void foo(){ std::unique_lock<std::mutex> lck(mtx, std::defer_lock); do_something_else(); lck.lock(); count ++ ; }
|
以下是一个简单的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <iostream> #include <thread> #include <mutex> #include <vector>
std::mutex mtx; int count;
void foo(int id){ std::unique_lock<std::mutex> lck(mtx, std::defer_lock); if(id & 1){ lck.lock(); count ++ ; std::cout << "thread " << id << " acquired the lock." << "\n"; }else{ std::cout << "thread " << id << " did not acquire the lock." << "\n"; } }
int main(){ std::vector<std::thread> v(20); for(int i = 0; i < 20; i ++ ) v[i] = std::thread(foo, i); for(auto& t : v) t.join();
std::cout << "final count = " << count << "\n";
return 0; }
|
在上面的代码中的 foo()
函数中,定义 std::unique_lock
对象时增加了 std::defer_lock
延迟加锁标记参数,表示稍后再进行加锁。当线程的标记为奇数时,我们手动地进行加锁( std::unique_lock
之后会自动进行解锁),然后对共享数据 count
进行 +1
操作;当线程地标记为偶数时,我们不进行加锁。
可能的运行结果如下:

可以看出,只有传入的线程标记为奇数时,线程才会获得锁并对共享变量 count
进行 +1
操作。一共 20 个线程,有 10 个线程互斥地获得锁,并对数据进行修改操作。因此最终 count
为 10
。
2.4 std::adopt_lock
假如一个线程已经拥有了互斥量的锁,此时互斥量已经被锁定,我们若要将锁的管理转移给 std::unique_lock
对象,需要在构造 std::unique_lock
时加上第二个参数 std::adopt_lock
。
例如下面这种形式:
1 2 3 4 5 6 7 8 9
| std::mutex mtx; int count;
void foo(){ mtx.lock(); std::unique_lock<std::mutex> lck(mtx, std::adopt_lock); count ++ ; }
|
下面是一个简单的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <iostream> #include <thread> #include <mutex> #include <vector>
std::mutex mtx; int count;
void foo(){ mtx.lock(); std::unique_lock<std::mutex> lck(mtx, std::adopt_lock); count ++ ; std::cout << "thread id = " << std::this_thread::get_id(); std::cout << ", count = " << count << "\n"; }
int main(){ std::vector<std::thread> v(10); for(auto& t : v) t = std::thread(foo); for(auto& t : v) t.join();
std::cout << "final count = " << count << "\n";
return 0; }
|
在上面的代码的 foo()
中,一开始已经调用 std::mutex::lock()
函数使线程获得了锁,然后定义一个 std::unique_lock
对象并加上 std::adopt_lock
参数,将锁的管理转移到 std::unique_lock
上。之后,就可以自动地进行解锁了,不需要再调用 std::mutex::unlock()
进行解锁。
可能的运行结果如下:

可以看出,多个线程互斥访问和修改共享数据,最终的结果为 10。
2.5 std::try_to_lock
我们也可以尝试构造一个 std::unique_lock
对象并尝试非阻塞地锁定互斥量。如果互斥量当前不可用,它不会阻塞线程,而是直接返回。此时我们可以通过 std::unique_lock
成员函数 owns_lock()
来判断是否获得了锁,如果获得了锁,成员函数 owns_lock()
返回 true
,否则返回 false
。
例如下面这种形式:
1 2 3 4 5 6 7 8 9 10 11 12
| std::mutex mtx; int count;
void foo(){ std::unique_lock<std::mutex> lck(mtx, std::try_to_lock); if(lck.owns_lock()){ count ++ ; std::cout << "Successfully get the lock." << "\n"; }else{ std::cout << "Unsuccessfully get the lock." << "\n"; } }
|
以下是一个简单的代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <iostream> #include <thread> #include <mutex> #include <vector>
std::mutex mtx; int count;
void foo(){ std::unique_lock<std::mutex> lck(mtx, std::try_to_lock); if(lck.owns_lock()){ count ++ ; std::cout << "Successfully get the lock." << "\n"; }else{ std::cout << "Unsuccessfully get the lock." << "\n"; } }
int main(){ std::vector<std::thread> v(20); for(auto& t : v) t = std::thread(foo); for(auto& t : v) t.join();
std::cout << "final count = " << count << "\n";
return 0; }
|
在上面的代码的 foo()
中,定义了一个 std::unique_lock
对象,并加上 std::try_to_lock
。我们用 std::unique_lock::owns_lock()
函数判断线程是否获得了锁,如果获得了锁,那么对共享数据进行访问和修改;否则,当前线程会直接返回,不会被阻塞并等待其他线程释放锁。
可能的运行结果如下:

可以看出只有少量的线程可以顺利获得锁,而其他没有成功获得锁的线程直接返回。
3 unique_lock 其他成员函数
上面的介绍中有了 std::unique_lock::lock()
、std::unique_lock::unlock()
以及 std::unique_lock::owns_lock()
成员函数。除这些函数之外,还有一些其他成员函数。
3.1 try_lock
std::unique_lock
的 try_lock
成员函数用于尝试以非阻塞的方式获取互斥量(mutex)的锁,通常需要搭配第二个参数 std::defer_lock
延迟加锁标记使用。有点类似于前面提到的构造函数参数 std::try_to_lock
的作用。
如果 std::unique_lock::try_lock()
返回值为 true
,表示成功获得了锁,接下来进行一系列操作;否则,无法成功获得锁,但是线程不会被阻塞并等待其他线程释放锁,而是会直接返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <iostream> #include <thread> #include <mutex> #include <vector>
std::mutex mtx; int count;
void foo() { std::unique_lock<std::mutex> lck(mtx, std::defer_lock); if(lck.try_lock()){ count ++ ; std::cout << "Successfully get the lock." << "\n"; std::cout << "thread id = " << std::this_thread::get_id(); std::cout << ", count = " << count << "\n"; }else{ std::cout << "Unsuccessfully get the lock." << "\n"; } }
int main() { std::vector<std::thread> v(10); for(auto& t : v) t = std::thread(foo); for(auto& t : v) t.join();
return 0; }
|
可能的运行结果如下:

3.2 release
std::unique_lock::release
是 std::unique_lock
类模板的一个成员函数。它的主要作用是释放 std::unique_lock
对象对互斥量(mutex)的所有权,返回值是 一个指向被管理的互斥量的指针,并将 std::unique_lock
对象与互斥量的关联断开。
之后例如转移给某个 std::mutex
互斥量,那么需要变为手动地加锁和解锁,管理互斥量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <iostream> #include <thread> #include <mutex>
std::mutex mtx; int count;
void foo() { std::unique_lock<std::mutex> lck(mtx); std::mutex *m = lck.release(); count ++ ; std::cout << "thread id = " << std::this_thread::get_id(); std::cout << ", count = " << count << "\n"; m->unlock(); }
int main() { std::thread t1(foo); std::thread t2(foo);
t1.join(); t2.join();
return 0; }
|
在上面的代码的 foo()
中,先创建了一个 std::unique_lock
对象并锁定了互斥量mtx
。然后通过 std::mutex* m = lck.release();
释放了 lck
对 mtx
的所有权,同时获取了指向 mtx
的指针。
之后,std::unique_lock
对象不再管理互斥量,必须手动调用 mtx->unlock()
进行解锁互斥量。
可能的运行结果如下:

4 简单应用实例
和之前 std::mutex
文章 「C++ 多线程」std::mutex,lock(), unlock() 中的问题相同:
- 一个线程用来接受用户命令(用数字表示命令),并把命令写入一个队列中
- 另一个线程从队列中读取命令,解析命令并执行一系列操作
程序中共享数据(数据结构) msg_que,消息队列同一时刻只允许一个线程进行操作(读取或写入)。因此 msg_que 是临界资源,我们需要对访问临界资源的代码加锁。
我们可以用 unique_lock
对需要互斥的部分进行改写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| #include <iostream> #include <thread> #include <mutex> #include <vector> #include <queue>
class MyClass{ std::queue<int> msg_que; std::mutex mtx; public: void in_msg_que(int num){ for(int i = 0; i < num; i ++ ){ std::cout << "in_msg_que() running, push data: " << i << "\n"; std::unique_lock<std::mutex> lck(mtx, std::try_to_lock); if(lck.owns_lock()){ msg_que.push(i); }else{ std::cout << "in_msg_que() running, but cannot get lock: " << i << "\n"; } } }
void out_msg_que(int num){ int command = 0; for(int i = 0; i < num; i ++ ){ if(pop_command(command)){ std::cout << "out_msg_que() running, command is: " << command << "\n"; }else{ std::cout << "out_msg_que() running, queue is empty: " << i << "\n"; } } }
bool pop_command(int& command){ std::unique_lock<std::mutex> lck(mtx); if(msg_que.empty()){ return false; } command = msg_que.front(); msg_que.pop(); return true; } };
int main(){ MyClass obj; std::thread out_msg_t(&MyClass::out_msg_que, &obj, 10000); std::thread in_msg_t(&MyClass::in_msg_que, &obj, 10000);
out_msg_t.join(); in_msg_t.join();
return 0; }
|
可能的运行结果如下:
