C++98
标准中并没有线程库的存在。C++11
中才提供了多线程的标准库,提供了thread
、mutex
、condition_variable
、atomic
等相关对象及功能功能。
概述
- C++ 11中与 Mutex 相关的类(包括锁类型)和函数都声明在
<mutex>
头文件中。 #include <mutex>
- ☃互斥对象 有四种类型:
I. std::mutex,最基本的 Mutex 类。
II. std::recursive_mutex,递归 Mutex 类。
III. std::time_mutex,定时 Mutex 类。
IV. std::recursive_timed_mutex,定时递归 Mutex 类。
- ☃ 锁对象 有两种类型:
I. std::lock_guard,RAII 机制,方便线程对互斥量上锁。
II. std::unique_lock,RAII 机制,方便线程对互斥量上锁,提供了更好的上锁和解锁控制。
- ☃ Other types:
I. std::once_flag。
II. std::adopt_lock_t。
III. std::defer_lock_t。
IV. std::try_to_lock_t。 - ☃ 函数:
I. std::try_lock,尝试同时对多个互斥对象 上锁。
II. std::adopt_lock_t。
III. std::defer_lock_t。
IV. std::try_to_lock_t。
1 互斥对象
1.1 mutex 构造函数
std::mutex
是C++11 中最基本的互斥量,std::mutex
对象提供独占所有权:不支持递归地对std::mutex
对象上锁。std::mutex
不支持 copy、move 构造,仅可以使用默认构造。
constexpr mutex() noexcept; // default
mutex(const mutex&) = delete; // 不支持copy、move 构造
1.2 mutex 成员函数
-std::mutex
包含以下成员函数:
void lock(); // 1. 获取锁
native_handle_type native_handle(); // 2. 实现定义的原生句柄对象。
bool try_lock(); // 3. 尝试锁住 mutex对象
void unlock(); // 4. 释放对互斥锁的所有权。
- ①
lock();
调用线程锁定std::mutex
(互斥锁) 对象,必要时阻塞:
I. 如果互斥锁当前未被任何线程锁定,则调用线程将锁定它(从此时开始,直到调用 unlock,该线程一直拥有该互斥锁)。
II. 如果互斥锁当前被另一个线程锁定,当前的调用线程阻塞,直到被另一个线程解锁。
III. 如果互斥锁被当前调用此函数的同一线程锁定,则会产生死锁。
- ②
unlock()
: 解锁,释放锁对象的所有权。
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex
std::mutex mtx;
void print_thread_id (int id) {
mtx.lock();
this_thread::sleep_for(chrono::milliseconds(300));
std::cout << "thread #" << id << '\n';
mtx.unlock();
}
int main () {
std::thread threads[10];
// spawn 10 threads:
for (int i=0; i<10; ++i)
threads[i] = std::thread(print_thread_id,i+1);
for (auto& th : threads)
th.join();
return 0;
}
thread #1
thread #2
thread #3
thread #4
thread #5
thread #6
thread #7
thread #8
thread #10
thread #9
- 每一个线程获取锁后,会
sleep 300
毫秒,因此后续的线程会同时申请锁,导致打印顺序并不是升序。
- ③
try_lock()
: 尝试锁定互斥锁,但不阻塞:
I. 如果互斥锁当前没有被任何线程锁定,则调用线程将锁定它。
II. 如果互斥锁当前被另一个线程锁定,则该函数将失败并返回false,而不阻塞(调用线程继续执行)。
III.如果互斥锁当前被调用此函数的同一线程锁定,则会产生死锁(具有未定义的行为)。
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex
volatile int count_(0); // 1. 全局变量
std::mutex mtx;
void attempt_10k_increases () {
for (int i=0; i<10000; ++i) {
if (mtx.try_lock()) {
// 2. 只有获得锁时才会 ++count
++count_;
mtx.unlock();
}
}
}
int main () {
std::thread threads[10];
// 3. 10个线程
for (int i=0; i<10; ++i)
threads[i] = std::thread(attempt_10k_increases);
for (auto& th : threads)
th.join();
std::cout << "Count: " << count_ << std::endl;
return 0;
}
Count: 9522
- 10 个线程同时执行,线程内部使用
try_lock()
函数获取锁,获取后++count;
。 - 但由于同一时间只能有一个线程能拥有锁,因此最终
count
值在1w 左右。
1.3 other
recursive_mutex
:与 mutex 对象所包含的成员函数相同,不同之处在于recursive_mutex
允许同一个线程对互斥量多次上锁(即递归上锁),time_mutex
recursive_timed_mutex
timed_mutex
2 锁对象
2.1 lock_guard
lock_guard
遵循RAII
来处理资源, 定义如下:
template <class Mutex> class lock_guard;
RAII
也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象。
lock_guard
的职责就是管理互斥对象mutex,在其构造函数中进行加锁,在其析构函数中进行解锁。- 最终的结果就是:创建即加锁,生命周期结束自动解锁。因此使用
lock_guard()
可以免去lock()
与unlock()
。 lock_guard()
成员函数仅有 构造函数、析构函数。其构造函数如下:
explicit lock_guard(mutex_type& m); // 1. locking
lock_guard(mutex_type& m, adopt_lock_t tag); // 2. 双参数-adopting
lock_guard(const lock_guard&) = delete; // 3. 复制构造
- 双参数构造函数第一个参数为
mutex
对象,第二个参数为adopt_lock
标识,表示构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定。
测试代码:
std::mutex m;
void func_1() {
lock_guard<mutex> g1(m); // 1. 构造函数只包含 mutex对象
std::cout << "func_1 get lock." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "func_1 unlock." << std::endl; // 作用域结束,自动解锁
}
void func_2() {
m.lock(); // 手动锁定
lock_guard<mutex> g2(m,adopt_lock); // 2. 构造函数包含两个参数
std::cout << "func_2 get lock." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "func_2 unlock." << std::endl;
}
int main() {
std::thread thread_1(func_1);
std::thread thread_2(func_2);
thread_1.join();
thread_2.join();
return 0;
}
func_1 get lock.
func_1 unlock.
func_2 get lock.
func_2 unlock.
- 可以看出,
thread_1
sleep 1秒钟,期间未释放锁,thread_2
.loc();
阻塞。 thread_1
运行结束,释放锁,thread_2
运行。
2.2 unique_lock 构造
unique_lock
以独占所有权的方式 管理mutex
对象,所谓独占所有权,就是没有其他的 unique_lock 对象同时拥有某个 mutex 对象的所有权。- 和
lock_guard
一样,unique_lock
对象也能保证在其自身析构时它所管理的 Mutex 对象能够被正确地解锁(即使没有显式地调用 unlock 函数)。构造函数如下所示:
unique_lock() noexcept; // 1. default
explicit unique_lock(mutex_type& m); // 2. locking
unique_lock(mutex_type& m, try_to_lock_t tag); // 3. try-locking
unique_lock(mutex_type& m, defer_lock_t tag) noexcept; // 4. deferred
unique_lock(mutex_type& m, adopt_lock_t tag); // 5. adopting
template <class Rep, class Period> // 6. locking for
unique_lock(mutex_type& m, const chrono::duration<Rep,Period>& rel_time);
template <class Clock, class Duration> // 7. locking until
unique_lock(mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time);
unique_lock(const unique_lock&) = delete; // 8. copy [deleted]
unique_lock(unique_lock&& x); // 8. move
- 新创建的 unique_lock 对象不管理任何 Mutex 对象。
- 新创建的 unique_lock 对象管理 m 对象,并通过调用
m.lock()
来获取锁(若未获取、则阻塞)。- 新创建的 unique_lock 对象管理 m 对象,并通过调用
m.try_lock()
对尝试获取锁,失败并不阻塞。- 新创建的 unique_lock 对象管理 m 对象,而不锁定 m 对象。
- 新创建的 unique_lock 对象管理 m 对象, m 应该是一个已经被当前线程锁住的 Mutex 对象。(并且当前新创建的 unique_lock 对象拥有对锁(Lock)的所有权)。
- 新创建的 unique_lock 对象管理 m 对象,并通过调用
m.try_lock_for(rel_time)
来锁住 Mutex对象 一段时间。- 新创建的 unique_lock 对象管理 m 对象,并通过调用
m.try_lock_until(abs_time)
来在某个时间点之前锁住 Mutex 对象。- unique_lock 对象 不存在复制构造,移动构造可正常使用。
测试代码:
std::mutex mtx; // mutex对象
void print_thread_id (int id) {
// 1. unique_lock对象获取 mutex的管理权,而不锁定它
std::unique_lock<std::mutex> lck(mtx,std::defer_lock);
lck.lock(); // 2. 获取锁,阻塞
std::cout << "thread #" << id << '\n';
lck.unlock();
}
int main () {
std::thread threads[10];
for (int i=0; i<10; ++i)
threads[i] = std::thread(print_thread_id,i+1);
for (auto& th : threads)
th.join();
return 0;
}
thread #1
thread #4
thread #5
thread #6
thread #3
thread #7
thread #2
thread #8
thread #9
thread #10
- 以上例子为
unique_lock
第四个构造函数的使用,获取mutex
对象的管理权,并不去锁定它。
2.2 unique_lock 构造
unique_lock
提供了以下成员函数:
void lock(); // 1. 手动加锁,未获取锁会阻塞
mutex_type* mutex() const noexcept; // 2. Get mutex: 返回指向mutex的指针
operator=(unique_lock&& x); // 3.1 移动赋值构造
operator=(const unique_lock&) = delete; // 3.2 复制构造 删除
// 4. 返回是否拥有锁,可将 unique_guard 对象作为判断参数,if(unique_guard)
explicit operator bool() const noexcept;
bool owns_lock() const noexcept; // 5. 返回对象是否拥有锁
mutex_type* release() noexcept; // 6. 释放 mutex 所有权,返回mutex指针
void swap(unique_lock& x) noexcept; // 7. 交换两个 unique_guard 对象的内容
bool try_lock(); // 8. 尝试获取锁,返回状态
bool try_lock_for(chrono...& rel_time); // 9. 在后续一段时间内尝试锁定互斥锁
bool try_lock_until(chrono...& abs_time); // 10.直至某一时刻为止,一直尝试锁定互斥锁
void unlock(); // 11.释放锁,对象状态为false,若调用该函数前
// 对象状态已经为false,则抛出异常
- 看了以上11个成员函数,感觉很像
unique_ptr
,release()
很像。 - 上一小节的例子中使用了
unique_locklock()
、unlock()
。此处使用测试代码 对 函数try_lock
、try_lock_for
、try_lock_until
功能进行讲解。
std::timed_mutex mtx;
void fireworks () {
std::unique_lock<std::timed_mutex> lck(mtx,std::defer_lock);
while (!lck.try_lock_for(std::chrono::milliseconds(200))) {
// 1. 等待200毫秒
std::cout << "-";
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // 2. 延迟1000毫秒
std::cout << "*\n";
}
int main () {
std::thread threads[10];
// spawn 10 threads:
for (int i=0; i<10; ++i)
threads[i] = std::thread(fireworks);
for (auto& th : threads)
th.join();
return 0;
}
------------------------------------*
----------------------------------------*
-----------------------------------*
------------------------------*
-------------------------*
--------------------*
---------------*
----------*
-----*
*
try_*()
函数功能相似,上述例子使用了try_lock_for()
。