C++ 多线程之互斥量(mutex)详解
作者:胖小迪 发布时间:2023-12-20 18:51:32
C++ 11中的互斥量,声明在 <mutex>
头文件中,互斥量的使用可以在各种方面,比较常用在对共享数据的读写上,如果有多个线程同时读写一个数据,那么想要保证多线程安全,就必须对共享变量的读写进行保护(上锁),从而保证线程安全。
互斥量主要有四中类型:
std::mutex
,最基本的 Mutex 类。std::recursive_mutex
,递归 Mutex 类。std::time_mutex
,限时 Mutex 类。std::recursive_timed_mutex
,限时递归 Mutex 类。
当然C++14和C++17各增加了一个:
std::shared_timed_mutex
,限时读写锁(C++14)std::shared_mutex
,读写锁(C++17)
std::mutex
构造函数
mutex();
mutex(const mutex&) = delete;
从上面的构造函数可以看出,std::mutex不允许拷贝构造,当然也不允许move,最初构造的mutex对象是处于未锁定状态的,若构造不成功会抛出 std::system_error
。
析构函数
~mutex();
销毁互斥。若互斥被线程占有,或在占有mutex时线程被终止,则会产生未定义行为。
lock
void lock();
锁定互斥,调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:
如果该互斥量当前没有被其他线程锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一直拥有该锁。
如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住,指导其他线程unlock该互斥量。
如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
try_lock
bool try_lock();
尝试锁住互斥量,立即返回。成功获得锁时返回 true ,否则返回 false。
如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况:
如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
unlock
void unlock();
解锁互斥。互斥量必须为当前执行线程所锁定(以及调用lock),否则行为未定义。
看下面一个简单的例子实现两个线程竞争全局变量g_num对其进行写操作,然后打印输出:
#include <iostream>
#include <chrono> // std::chrono
#include <thread> // std::thread
#include <mutex> // std::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 << "th" << 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();
}
加了互斥量实现有序的写操作并输出:
th0 => 1
th1 => 2
th0 => 3
th1 => 4
th1 => 5
th0 => 6
如果不增加mutex包含,可能输出就不是有序的打印1到6,如下:
thth01 => 2 => 2
th1 => 3
th0 => 4
th0 => 5
th1 => 6
std::recursive_mutex
如上面所说的,如果使用std::mutex,如果一个线程在执行中需要再次获得锁,会出现死锁现象。要避免这种情况下就需要使用递归式互斥量std::recursive_mutex
,它不会产生上述的死锁问题,可以理解为同一个线程多次获得锁“仅仅增加锁的计数”,同时,必须要确保unlock和lock的次数相同,其他线程才可能取得这个mutex。它的接口与std::mutex的完全一样,用法也基本相同除了可重入(必须同一线程才可重入,其他线程需等待),看下面的例子:
#include <iostream>
#include <thread>
#include <mutex>
class X {
std::recursive_mutex m;
std::string shared;
public:
void fun1() {
m.lock();
shared = "fun1";
std::cout << "in fun1, shared variable is now " << shared << '\n';
m.unlock();
}
void fun2() {
m.lock();
shared = "fun2";
std::cout << "in fun2, shared variable is now " << shared << '\n';
fun3(); // 递归锁在此处变得有用
std::cout << "back in fun2, shared variable is " << shared << '\n';
m.unlock();
}
void fun3() {
m.lock();
shared = "fun3";
std::cout << "in fun3, shared variable is now " << shared << '\n';
m.unlock();
}
};
int main()
{
X x;
std::thread t1(&X::fun1, &x);
std::thread t2(&X::fun2, &x);
t1.join();
t2.join();
}
在fun2中调用fun3,而fun3中还使用了lock和unlock,只有递归式互斥量才能满足当前情况。
输出如下:
in fun1, shared variable is now fun1
in fun2, shared variable is now fun2
in fun3, shared variable is now fun3
back in fun2, shared variable is fun3
std::time_mutex
timed_mutex增加了带时限的try_lock。即try_lock_for
和try_lock_until
。
try_lock_for尝试锁互斥。阻塞直到超过指定的 timeout_duration
或得到锁,取决于何者先到来。成功获得锁时返回 true,否则返回false 。函数原型如下:
template< class Rep, class Period >
bool try_lock_for( const std::chrono::duration<Rep,Period>& timeout_duration );
若timeout_duration
小于或等于timeout_duration.zero()
,则函数表现同try_lock()
。由于调度或资源争议延迟,此函数可能阻塞长于timeout_duration
。
#include <iostream>
#include <sstream>
#include <thread>
#include <chrono>
#include <vector>
#include <mutex>
std::timed_mutex mutex;
using namespace std::chrono_literals;
void do_work(int id) {
std::ostringstream stream;
for (int i = 0; i < 3; ++i) {
if (mutex.try_lock_for(100ms)) {
stream << "success ";
std::this_thread::sleep_for(100ms);
mutex.unlock();
} else {
stream << "failed ";
}
std::this_thread::sleep_for(100ms);
}
std::cout << "[" << id << "] " << stream.str() << std::endl;
}
int main() {
// try_lock_for
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(do_work, i);
}
for (auto& t : threads) {
t.join();
}
}
[3] failed success failed
[0] success failed success
[2] failed failed failed
[1] success success success
try_lock_until也是尝试锁互斥。阻塞直至抵达指定的timeout_time
或得到锁,取决于何者先到来。成功获得锁时返回 true,否则返回false。
timeout_time与上面的timeout_duration不一样,timeout_duration表示一段时间,比如1秒,5秒或者10分钟,而timeout_time表示一个时间点,比如说要等到8点30分或10点24分才超时。
使用倾向于timeout_time
的时钟,这表示时钟调节有影响。从而阻塞的最大时长可能小于但不会大于在调用时的 timeout_time - Clock::now() ,依赖于调整的方向。由于调度或资源争议延迟,函数亦可能阻塞长于抵达timeout_time
之后。同try_lock()
,允许此函数虚假地失败并返回false,即使在 timeout_time
前的某点任何线程都不锁定互斥。函数原型如下:
template< class Clock, class Duration >
bool try_lock_until( const std::chrono::time_point<Clock,Duration>& timeout_time);
看下面的例子:
#include <iostream>
#include <sstream>
#include <thread>
#include <chrono>
#include <vector>
#include <mutex>
std::timed_mutex mutex;
using namespace std::chrono;
void do_work() {
mutex.lock();
std::cout << "thread 1, sleeping..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(4));
mutex.unlock();
}
void do_work2() {
auto now = std::chrono::steady_clock::now();
if (mutex.try_lock_until(now + 5s)) {
auto end = steady_clock::now();
std::cout << "try_lock_until success, ";
std::cout << "time use: " << duration_cast<milliseconds>(end-now).count()
<< "ms." << std::endl;
mutex.unlock();
} else {
auto end = steady_clock::now();
std::cout << "try_lock_until failed, ";
std::cout << "time use: " << duration_cast<milliseconds>(end-now).count()
<< "ms." << std::endl;
}
}
int main() {
// try_lock_until
std::thread t1(do_work);
std::thread t2(do_work2);
t1.join();
t2.join();
}
获得锁时输出:
thread 1, sleeping...
try_lock_until success, time use: 4000ms.
修改一下,让其超时,输出:
thread 1, sleeping...
try_lock_until failed, time use: 5000ms.
std::recursive_timed_mutex
以类似std::recursive_mutex的方式,recursive_timed_mutex
提供排他性递归锁,同线程可以重复获得锁。另外,recursive_timed_mutex
通过try_lock_for
与try_lock_until
方法,提供带时限地获得recursive_timed_mutex
锁,类似std::time_mutex
。
std::shared_mutex
c++ 17 新出的具有独占模式和共享模式的锁。共享模式能够被std::shared_lock
(这个后面再详细将)占有。
std::shared_mutex 是读写锁,把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
它提供两种访问权限的控制:共享性(shared)和排他性(exclusive)。通过lock/try_lock
获取排他性访问权限(仅有一个线程能占有互斥),通过lock_shared/try_lock_shared
获取共享性访问权限(多个线程能共享同一互斥的所有权)。这样的设置对于区分不同线程的读写操作特别有用。
std::shared_mutex
通常用于多个读线程能同时访问同一资源而不导致数据竞争,但只有一个写线程能访问的情形。比如,有多个线程调用shared_mutex.lock_shared()
,多个线程都可以获得锁,可以同时读共享数据,如果此时有一个写线程调用 shared_mutex.lock()
,则读线程均会等待该写线程调用shared_mutex.unlock()
。对于C++11 没有提供读写锁,可使用 boost::shared_mutex
。
std::shared_mutex
新增加的三个接口:
void lock_shared();
bool try_lock_shared();
void unlock_shared();
一个简单例子如下:
#include <iostream>
#include <mutex> // 对于 std::unique_lock
#include <shared_mutex>
#include <thread>
class ThreadSafeCounter {
public:
ThreadSafeCounter() = default;
// 多个线程/读者能同时读计数器的值。
unsigned int get() const {
std::shared_lock<std::shared_mutex> lock(mutex_);
return value_;
}
// 只有一个线程/写者能增加/写线程的值。
void increment() {
std::unique_lock<std::shared_mutex> lock(mutex_);
value_++;
}
// 只有一个线程/写者能重置/写线程的值。
void reset() {
std::unique_lock<std::shared_mutex> lock(mutex_);
value_ = 0;
}
private:
mutable std::shared_mutex mutex_;
unsigned int value_ = 0;
};
int main() {
ThreadSafeCounter counter;
auto increment_and_print = [&counter]() {
for (int i = 0; i < 3; i++) {
counter.increment();
std::cout << std::this_thread::get_id() << ' ' << counter.get() << '\n';
// 注意:写入 std::cout 实际上也要由另一互斥同步。省略它以保持示例简洁。
}
};
std::thread thread1(increment_and_print);
std::thread thread2(increment_and_print);
thread1.join();
thread2.join();
}
// 解释:下列输出在单核机器上生成。 thread1 开始时,它首次进入循环并调用 increment() ,
// 随后调用 get() 。然而,在它能打印返回值到 std::cout 前,调度器将 thread1 置于休眠
// 并唤醒 thread2 ,它显然有足够时间一次运行全部三个循环迭代。再回到 thread1 ,它仍在首个
// 循环迭代中,它最终打印其局部的计数器副本的值,即 1 到 std::cout ,再运行剩下二个循环。
// 多核机器上,没有线程被置于休眠,且输出更可能为递增顺序。
可能的输出:
139847802500864 1
139847802500864 2
139847802500864 3
139847794108160 4
139847794108160 5
139847794108160 6
std::shared_timed_mutex
它是从C++14 才提供的限时读写锁:std::shared_timed_mutex
。
对比std::shared_mutex
新增下面两个接口,其实这两个接口与上面讲到的std::timed_mutex
的try_lock_for
和try_lock_until
类似。都是限时等待锁。只不过是增加了共享属性。
template< class Rep, class Period >
bool try_lock_shared_for( const std::chrono::duration<Rep,Period>& timeout_duration );
template< class Clock, class Duration >
bool try_lock_shared_until( const std::chrono::time_point<Clock,Duration>& timeout_time );
来源:https://blog.csdn.net/iuices/article/details/123099747


猜你喜欢
- 通常用java来打包文件生成压缩文件后,有如下两个地方会出现乱码 :1、内容的中文乱码问题,这个问题网上很多人给出了解决方法,主要有两种方法
- 目录1.C语音的字符串有两种1.1字符数组1.2字符指针2.字符串常用的方法2.1strcpy字符串拼接2.2strchr字符串中查找字符2
- 目录1、对于A、B两种排队方式,说法正确的是2、Inter-process communication (IPC) is the trans
- 前言因为工作原因,需要在项目中集成dubbo,所以去查询dubbo相关文档,发现dubbo目前已经不更新了,所以把目光投向了dubbox,d
- 一、堆的创建1、向下调整(以小堆为例) 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent
- Android四种数据存储的应用方式作为一个完整的应用程序,数据存储操作是必不可少的。因此,Android系统一共提供了四种数据存储方式。分
- 本文实例为大家分享了java生成随机验证码图片的具体代码,供大家参考,具体内容如下1.controller /**  
- 本篇要点简单描述浮点数十进制转二进制精度丢失的原因。介绍几种创建BigDecimal方式的区别。整理了高精度计算的工具类。学习了阿里巴巴Ja
- 上一节我们完成了使用DataGrid显示所有商品信息,这节我们开始添加几个功能:添加、更新、删除和查询。首先我们实现下前台的显示,然后再做后
- 问题在Android开发中,遇到一个问题,是ListView嵌套GridView,需要点击整个ListView的Item进行跳转。但是在点击
- 最近在项目过程中遇到了保存数据的需求,对实体类的部分数据进行保存,打算采用反射+自定义特性来实现数据保存,利于扩展1. 采用反射实现能够灵活
- C# 的类型系统可分为两种类型,一是值类型,一是引用类型,这个每个C#程序员都了解。还有托管堆,栈,ref,out等等概念也是每个C#程序员
- hive的表数据是可以同步到impala中去的。一般impala是提供实时查询操作的,像比较耗时的入库操作我们可以使用hive,然后再将数据
- 需求:有一个列表,列表中有一个edittext(只能输整形),外部有一个整形变量Int,每次改变列表中其中一项的edittext的值时,外部
- 隐藏标题栏基于xml<application android:theme="@style/Them
- 本文实例讲述了Android发送xml数据给服务器的方法。分享给大家供大家参考。具体如下:一、发送xml数据:public static v
- 问题在讨论原子性操作时,我们经常会听到一个说法:任意单个volatile变量的读写具有原子性,但是volatile++这种操作除外。所以问题
- 新增获取自增列id1、实体类定义注意:@TableId(value = “id”, type = I
- 现在Android上架各大平台都要求App首页添加一个弹框,显示用户协议以及一些隐私政策,不然上架各大平台,现在就来简单的实现一下这个对话框
- 在 Java 中,null 是一个表示“空值”的特殊值。相信大家都很了解 null 在 Java 中