std::thread线程库详解(2)
目录
- 最基本的锁 std::mutex
- 递归锁 std::recursive_mutex
- 共享锁 std::shared_mutex (C++17)
简介
上一篇博文中,介绍了一下如何创建一个线程,分别是 std::thread
和 std::jthread (C++20)
。这两种方法相似, std::jthread
相对来说,更加方便一些,具体可以再看看原来的博文, std::thread线程详解(1) 。
这一次,我将介绍一下,多线程的锁。锁在多线程中是使用非常广泛的。是多线程中最常见的同步方式。主要介绍的锁有 mutex
, recursive_mutex
, shared_mutex
。
最基本的锁 std::mutex
使用
std::mutex
是最基本的锁,也是最常见的锁。它提供了最基本的多线程编程同步方法。
using namespace std::chrono_literals; std::mutex g_mutex; void thread_func() { g_mutex.lock(); std::cout << "Thread out 1: " << std::this_thread::get_id() << std::endl;; std::this_thread::sleep_for(1s); std::cout << "Thread out 2: " << std::this_thread::get_id() << std::endl;; g_mutex.unlock(); } int main() { std::cout << "Mutex Test." << std::endl; std::thread thread1(thread_func); std::thread thread2(thread_func); thread1.join(); thread2.join(); return 0; }
以上示例中,只有一个线程函数 thread_func
,它的工作很简单:
首先对 g_mutex
加锁,然后输出一段字符串,接着休眠1s,输出第二段字符串,最后对 g_mutex
进行解锁。
输出结果如下:
锁的本质是解决多线程对同一资源竞争读写的问题。这里我们的资源是标准输出 std::cout
。锁的存在让输出有序,可预测了。
方法和属性
-
lock()
为对象加锁,如果已经被锁了,则阻塞线程; -
try_lock()
尝试加锁,如果已经被加锁,则返回false,否则将对其进行加锁并返回true; -
unlock()
为对象解锁,通常和加锁(lock()
,try_lock()
)成对出现; -
native_handle()
返回锁的POSIX标准对象。
递归锁 std::recursive_mutex
std::recursive_mutex
是一个递归锁,方法和使用都和 std::mutex
类似。唯一的不同是, std::mutex
在同一时间,只允许加锁一次,而 std::revursive_mutex
允许同一线程下进行多次加锁。如:
// 定义递归锁 std::recursive_mutex g_mutex; // 线程函数 void thread_func(int thread_id, int time) { g_mutex.lock(); std::cout << "Thread " << thread_id << ": " << time << std::endl; if (time != 0) thread_func(thread_id, time - 1); g_mutex.unlock(); } // 初始化线程 std::thread thread1(thread_func, 1, 3); std::thread thread2(thread_func, 2, 4);
这一次的方法和之前的略有不同,为了更加直观的观察不同的线程,这次是在输入的时候输入一个标志来区分不同的线程。可以清楚的看到,这是一个递归函数,每次调用的时候都将time减少1,直到其变为0。需要注意的是,在递归的时候并没有释放锁,而是直接进入,因此在第二层遍历的时候,又会对 g_mutex
进行一次加锁,如果是普通的锁,次数将会阻塞进程,变成死锁。但是此时使用的是递归锁,它允许在同一个线程,多次加锁,因此这个程序可以成功运行,并获得输出。
递归锁的方法和普通锁的方法类似。
共享锁 std::shared_mutex (C++17)
std::shared_mutex
在C++14已经存在了,但是在C++14中的 std::shared_mutex
是带timing的版本的读写锁(也就是说,C++14中的 std::shared_mutex
等于C++17中的 std::shared_timed_mutex
)。读写锁有两种加锁的方式,一种是 shared_lock()
,另一种 lock()
。 shared_lock
是读模式,而 lock
是写模式。读写锁允许多个读加锁,而写加锁和其他所有加锁互斥。即同一时间下:
- 允许多个线程同时读;
- 只允许一个线程写;
- 写的时候不允许读,读的时候不允许写。
示例:
// 共享锁 std::shared_mutex g_mutex; // 读线程 1 void thread_read_1_func(int thread_id) { // 第一个获取读权限 g_mutex.lock_shared(); std::cout << "Read thread " << thread_id << " out 1." << std::endl; // 睡眠2s,等待读线程2,获取读权限,确认可以多个线程进行读加锁 std::this_thread::sleep_for(2s); std::cout << "Read thread " << thread_id << " out 2." << std::endl; // 解锁读 g_mutex.unlock_shared(); } void thread_read_2_func(int thread_id) { // 睡眠500ms,确保读线程1先获取锁 std::this_thread::sleep_for(500ms); g_mutex.lock_shared(); std::cout << "Read thread " << thread_id << " out 1." << std::endl; std::this_thread::sleep_for(3s); std::cout << "Read thread " << thread_id << " out 2." << std::endl; g_mutex.unlock_shared(); } void thread_write_1_func(int thread_id) { // 确保读线程先获得锁,确认读写互斥 std::this_thread::sleep_for(300ms); g_mutex.lock(); std::cout << "Write thread " << thread_id << " out 1." << std::endl; g_mutex.unlock(); }
其输出为:
带超时的锁
上面介绍的所有的锁,都带有超时版本。即 timed_mutex
, recursive_timed_mutex
, shared_timed_mutex
。他们使用时,和普通版本类似,不过 try_lock
方法多了两个超时的版本 try_lock_for
和 try_lock_until
。调用这一函数时,如果锁已经被获取了,线程将会阻塞一段时间,如果这一段时间内,获取到了锁则返回 true
,否则返回 false
这里我们只介绍 timed_mutex
,其他的类似。
void thread_func(int thread_id) { if (!g_mutex.try_lock_for(0.5s)) return; std::cout << "Thread out 1: " << thread_id << std::endl;; std::this_thread::sleep_for(1s); std::cout << "Thread out 2: " << thread_id << std::endl;; g_mutex.unlock(); g_mutex.native_handle(); }
其输出为:
可以看到,这里只有一个线程有输出,另一个线程,在等待0.5s后直接退出了(没有获取到锁)。
总结
本文主要介绍了三种不同的锁,普通锁,递归锁,读写锁。三个锁有着不一样的使用方法,但是可以确定的是,过多的使用锁,会导致程序中的串行部分过多,并行效果不好。因此对于锁的使用,需要尽量的克制,尽量的合理。
下一篇文章将介绍锁的管理。