首页 > 编程语言 >C++ Mutex

C++ Mutex

时间:2024-10-24 22:52:41浏览次数:3  
标签:std lock C++ 互斥 mutex 线程 Mutex include

定义

  1. 互斥量可以使用在各种方面,常用在对共享数据的读写上。如果有多线程读写一个数据,那么想要保证线程安全,必须对共享变量的读写上锁

头文件

#include <mutex>

类型

  1. std::mutex,最基本的Mutex类
  2. std::recursive_mutex,递归Mutex
  3. std::time_mutex,限时Mutex
  4. std::recursive_timed_mutex,限时递归Mutex类
  5. std::shared_timed_mutex,限时读写锁(C++14)
  6. std::shared_mutex,读写锁(C++17)

std::mutex

构造函数

mutex() noexcept = default;
~mutex() = default;

mutex(const mutex&) = delete;
mutex& operator=(const mutex&) = delete;
  1. std::mutex不允许拷贝构造
  2. 销毁互斥,若互斥被线程占有,或在占有mutex时线程被终止,则会产生未定义行为

用法

lock

锁定互斥,线程将锁住该互斥量,立即返回
无返回值

  1. 如果该互斥量当前没有被其他线程锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一直拥有该锁。
  2. 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住,直到其他线程unlock该互斥量。
  3. 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

PS:阻塞≠死锁,阻塞大概率会通畅,死锁一旦产生,则程序结束

try_lock

尝试锁住互斥量,立即返回
成功=True,反之=False

  1. 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用unlock释放互斥量
  2. 如果当前互斥量被其他线程锁住,则当前调用线程返回false,而并不会被阻塞掉
  3. 果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
unlock

无返回值
解锁互斥。互斥量必须为当前执行线程所锁定(以及调用lock),否则行为未定义。

example

#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

std::recursive_mutex

定义

递归式互斥锁,同一个线程可以多次获得锁,而不会死锁

PS:lockunlock的次数必须相等,其他线程才能获得锁

用法

成员与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();
}

输出

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

定义

增加了带时限的try_lock

用法

try_lock_for

尝试获得锁,若无法立即得到,则最多阻塞timeout_duration或在此期间获得锁,指的是时间段
成功=True,反之=False

example
#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

尝试锁互斥。直到到达指定的时间才超时,指的是时间点
成功=True,反之=False

example
#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.

std::recursive_timed_mutex

定义

以类似std::recursive_mutex的方式,recursive_timed_mutex提供排他性递归锁,同线程可以重复获得锁。另外,recursive_timed_mutex通过try_lock_fortry_lock_until方法,提供带时限地获得recursive_timed_mutex锁,类似std::time_mutex

std::shared_mutex

定义

c++ 17 新出的具有独占模式和共享模式的锁。

std::shared_mutex 是读写锁,把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。

它提供两种访问权限的控制:共享性(shared)和排他性(exclusive)。通过lock/try_lock获取排他性访问权限(仅有一个线程能占有互斥),通过lock_shared/try_lock_shared获取共享性访问权限(多个线程能共享同一互斥的所有权)。这样的设置对于区分不同线程的读写操作特别有用。

std::shared_mutex通常用于多个读线程能同时访问同一资源而不导致数据竞争,但只有一个写线程能访问的情形。

#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_mutextry_lock_fortry_lock_until类似。都是限时等待锁。只不过是增加了共享属性。

互斥锁

  1. mutex,用于保证在任何时刻,都只能有一个线程访问该对象。 当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
  2. 要么锁住、要么不锁住

读写锁

  1. 相较于互斥锁,读写锁允许更高的并行性
  2. 也称为“共享-独占锁”
  3. 当处于写加锁时,在其解锁之前,所有尝试加锁线程都会阻塞
  4. 当处于读加锁时,所有试图对其加锁的线程都可以得到访问权
  5. 写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。

总结

  1. 由于它们额外的复杂性,读/写锁std::shared_mutexstd::shared_timed_mutex优于普通锁std::mutexstd::timed_mutex的情况比较少见。但是理论上确实存在。
  2. 如果在频繁但短暂的读取操作场景,读/写互斥不会提高性能。它更适合于读取操作频繁且耗时的场景。当读操作只是在内存数据结构中查找时,很可能简单的锁会胜过读/写锁。
  3. 如果读取操作的开销非常大,并且您可以并行处理许多操作,那么在某些时候增加读写比率应该会导致读取/写入器性能优于排他锁的情况。断点在哪里取决于实际工作量。
  4. 另请注意,在持有锁的同时执行耗时的操作通常是一个坏兆头。可能有更好的方法来解决问题,然后使用读/写锁。
  5. 还要注意,在使用mutex时,要时刻注意lock()与unlock()的加锁临界区的范围,不能太大也不能太小,太大了会导致程序运行效率低下,大小了则不能满足我们对程序的控制。并且我们在加锁之后要及时解锁,否则会造成死锁,lock()与unlock()应该是成对出现。

参考文章

C++ 多线程:互斥量(mutex)_c++ 互斥-CSDN博客

标签:std,lock,C++,互斥,mutex,线程,Mutex,include
From: https://blog.csdn.net/weixin_44623642/article/details/143196952

相关文章

  • 每日OJ题_牛客_DP10最大子矩阵_二维前缀和_C++_Java
    目录牛客_DP10最大子矩阵_二维前缀和题目解析C++代码Java代码牛客_DP10最大子矩阵_二维前缀和最大子矩阵_牛客题霸_牛客网(nowcoder.com)描述:        已知矩阵的大小定义为矩阵中所有元素的和。给定一个矩阵,你的任务是找到最大的非空(大小至少是1*1)子矩......
  • 【子项目:命令系统(Command System)】C++自制命令系统( 开发ing | 踩坑记录 )
    项目背景在某一项目中,遇到了需要自制命令系统的需求,而这个模块的复用性很高,因此单独拉出来做一个子项目更新日志[2024.10.15-10:00]增项目进度----[2024.10.1510:00]----首先实现最基础的输入输出功能,用std::getline读入行再分割成字符串数组-main.cpp#include......
  • 面试华为遇到了经典动态规划题 - 怎么用go、C++进行编程解决
    华为的知名很大程度上源自于在经历过被美国的制裁和打压后不仅撑住了,而且还做出了相对于自己来说技术进步程度很大的芯片​。这是一个不小的成绩​。从个人的角度上来说,华为是最难进的一家大公司之一,它的面试标准很严格​。这不仅是筛选人才,在某种程度上来说也是由于求职市场......
  • 【C++干货篇】——C/C++内存管理
    【C++干货篇】——C/C++内存管理文章目录【C++干货篇】——C/C++内存管理1.C/C++内存分布1.1静态区/数据段:1.2常量区/代码段:1.3栈:1.4堆:1.5.内存映射区:2.C语言中动态内存管理方式:`malloc/calloc/realloc/free`1.`malloc`2.`calloc`3.`realloc`总结3.C++内存管理方......
  • Visual Studio 2022工作原理及相关配置参数(干货满满)——C++
    最近工作有点忙,毕业也没多久,确实在企业和学校还是有很大的差距的,这段时间学到了很多很多,也没时间顾及博客了,刚好趁着这个1024稍微放慢脚步,总结总结。最近用VisualStudio比较频繁,也学到了很多相关的内容,借此博文简单记录一下,全是个人理解,若有地方理解有误还请各位大佬评论......
  • C++学习笔记2——函数重载
    1.函数重载1.1默认参数C++新增的默认参数指的是函数调用省略实参时自动调用的一个值。通过函数原型设置函数的默认参数,函数定义与没有默认参数时完全相同。如以下函数原型:char*left(constchar*str,intn=1);调用时如果省略参数n,则它的值将为1;否则传入的值将......
  • C++ 双端队列实现
    #include<iostream>usingnamespacestd;#defineullisize_ttemplate<classT>classDualStack{private: structNode{ Tdata; Node*next; }; Node*head,*tail; Node*p; ullilength;public: DualStack(){ head=NULL; length=0......
  • C++ STL queue 的实现
    求点赞,求关注,求评论求点赞,求关注,求评论求点赞,求关注,求评论求点赞,求关注,求评论求点赞,求关注,求评论求点赞,求关注,求评论求点赞,求关注,求评论这篇文章很短,直接给代码:#include<iostream>usingnamespacestd;template<classT>classQueue{protected: structnode......
  • C++学习路线(二十二)
    构造函数构造函数作用在创建一个新的对象时,自动调用的函数,用来进行“初始化”工作:对这个对象内部的数据成员进行初始化。构造函数特点1.自动调用(在创建新对象时,自动调用)2.构造函数的函数名,和类名相同3.构造函数没有返回类型4.可以有多个构造函数(即函数重载形式)构......
  • 计算机毕业设计项目推荐:大学生实习成绩评价系统的设计与实现38147(开题答辩+程序定制+
    摘 要21世纪的今天,随着社会的不断发展与进步,人们对于信息科学化的认识,已由低层次向高层次发展,由原来的感性认识向理性认识提高,管理工作的重要性已逐渐被人们所认识,科学化的管理,使信息存储达到准确、快速、完善,并能提高工作管理效率,促进其发展。论文主要是对大学生实习成绩......