C++ 多线程之互斥量(mutex)详解

目录
  • std::mutex
  • std::recursive_mutex
  • std::time_mutex
  • std::recursive_timed_mutex
  • std::shared_mutex
  • std::shared_timed_mutex
  • 总结

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_fortry_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_fortry_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_mutextry_lock_fortry_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 );

总结

由于它们额外的复杂性,读/写锁std::shared_mutex , std::shared_timed_mutex优于普通锁std::mutexstd::timed_mutex的情况比较少见。但是理论上确实存在。

如果在频繁但短暂的读取操作场景,读/写互斥不会提高性能。它更适合于读取操作频繁且耗时的场景。当读操作只是在内存数据结构中查找时,很可能简单的锁会胜过读/写锁。

如果读取操作的开销非常大,并且您可以并行处理许多操作,那么在某些时候增加读写比率应该会导致读取/写入器性能优于排他锁的情况。断点在哪里取决于实际工作量。

另请注意,在持有锁的同时执行耗时的操作通常是一个坏兆头。可能有更好的方法来解决问题,然后使用读/写锁。

还要注意,在使用mutex时,要时刻注意lock()与unlock()的加锁临界区的范围,不能太大也不能太小,太大了会导致程序运行效率低下,大小了则不能满足我们对程序的控制。并且我们在加锁之后要及时解锁,否则会造成死锁,lock()与unlock()应该是成对出现。

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • 浅谈c++11线程的互斥量

    为什么需要互斥量 在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源.这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的. #define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <string> #include <chrono> #include <thread>

  • c# mutex互斥量的深入解析

    互斥锁(Mutex) 互斥锁是一个互斥的同步对象,意味着同一时间有且仅有一个线程可以获取它. 互斥锁可适用于一个共享资源每次只能被一个线程访问的情况  函数: //创建一个处于未获取状态的互斥锁 Public Mutex(); //如果owned为true,互斥锁的初始状态就是被主线程所获取,否则处于未获取状态 Public Mutex(bool owned); 如果要获取一个互斥锁.应调用互斥锁上的WaitOne()方法,该方法继承于Thread.WaitHandle类 它处于等到状态直至所调

  • 互斥量mutex的简单使用(实例讲解)

    几个重要的函数: #include <pthread.h> int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutex_t *restrict attr);    //初始化mutex int pthread_mutex_destroy(pthread_mutex_t *mutex);  //如果mutex是动态分配的,则释放内存前调用此函数. int pthread_mutex_lock(pthrea

  • C++ 多线程之互斥量(mutex)详解

    目录 std::mutex std::recursive_mutex std::time_mutex std::recursive_timed_mutex std::shared_mutex std::shared_timed_mutex 总结 C++ 11中的互斥量,声明在 <mutex> 头文件中,互斥量的使用可以在各种方面,比较常用在对共享数据的读写上,如果有多个线程同时读写一个数据,那么想要保证多线程安全,就必须对共享变量的读写进行保护(上锁),从而保证线程安全. 互斥量主要有四中类型

  • Java多线程之哲学家就餐问题详解

    一.题目 教材提供一个哲学家就餐问题的解决方案的框架.本问题要求通过pthreads 互斥锁来实现这个解决方案. 哲学家 首先创建 5 个哲学家,每个用数字 0~4 来标识.每个哲学家作为一个单独的 线程运行. 可使用 Pthreads 创建线程.哲学家在思考和吃饭之间交替.为了模拟这两种活动,请让线程休眠 1 到 3 秒钟.当哲学家想要吃饭时,他调用函数: pickup_forks(int philosopher _number) 其中,philosopher _number 为想吃饭哲学家的

  • Java多线程之搞定最后一公里详解

    目录 绪论 一:线程安全问题 1.1 提出问题 1.2 不安全的原因 1.2.1 原子性 1.2.2 代码"优化" 二:如何解决线程不安全的问题 2.1 通过synchronized关键字 2.2 volatile 三:wait和notify关键字 3.1 wait方法 3.2 notify方法 3.3 wait和sleep对比(面试常考) 四:多线程案例 4.1 饿汉模式单线程 4.2 懒汉模式单线程 4.3 懒汉模式多线程低性能版 4.4懒汉模式-多线程版-二次判断-性能高 总结

  • Linux多线程使用互斥量同步线程

    本文将会给出互斥量的详细解说,并用一个互斥量解决上一篇文章中,要使用两个信号量才能解决的只有子线程结束了对输入的处理和统计后,主线程才能继续执行的问题. 一.什么是互斥量 互斥量是另一种用于多线程中的同步访问方法,它允许程序锁住某个对象,使得每次只能有一个线程访问它.为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作之后解锁. 二.互斥量的函数的使用 它们的定义与使用信号量的函数非常相似,它们的定义如下: #include <pthread.h> int pthre

  • Python多线程编程之threading模块详解

    一.介绍 线程是什么?线程有啥用?线程和进程的区别是什么? 线程是操作系统能够进行运算调度的最小单位.被包含在进程中,是进程中的实际运作单位.一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务. 二.Python如何创建线程 2.1 方法一: 创建Thread对象 步骤: 1.目标函数 2.实例化Thread对象 3.调用start()方法 import threading # 目标函数1 def fun1(num): for i in range(

  • 程序猿必须要掌握的多线程安全问题之锁策略详解

    一.常见的锁策略 1.1 乐观锁 乐观锁:乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正 式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如 何去做.乐观锁的性能比较高. 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会 上锁,这样别人想拿这个数据就会阻塞直到它拿到锁. 悲观锁的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程:所以性能不高. 乐观锁的问题:并不总是能处理

  • Java多线程读写锁ReentrantReadWriteLock类详解

    目录 ReentrantReadWriteLock 读读共享 写写互斥 读写互斥 源码分析 写锁的获取与释放 读锁的获取与释放 参考文献 真实的多线程业务开发中,最常用到的逻辑就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务),这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的.所以在JDK中提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率. 读写锁表示两个锁,一个是读操作相关的锁

  • Java多线程同步工具类CountDownLatch详解

    目录 简介 核心方法 CountDownLatch如何使用 CountDownLatch运行流程 运用场景 总结 简介 CountDownLatch是一个多线程同步工具类,在多线程环境中它允许多个线程处于等待状态,直到前面的线程执行结束.从类名上看CountDown既是数量递减的意思,我们可以把它理解为计数器. 核心方法 countDown():计数器递减方法. await():使调用此方法的线程进入等待状态,直到计数器计数为0时主线程才会被唤醒. await(long, TimeUnit):在

  • Spring 多线程下注入bean问题详解

    本文介绍了Spring 多线程下注入bean问题详解,分享给大家,具体如下: 问题 Spring中多线程注入userThreadService注不进去,显示userThreadService为null异常 代码如下: public class UserThreadTask implements Runnable { @Autowired private UserThreadService userThreadService; @Override public void run() { AdeUs

  • python多进程和多线程究竟谁更快(详解)

    python3.6 threading和multiprocessing 四核+三星250G-850-SSD 自从用多进程和多线程进行编程,一致没搞懂到底谁更快.网上很多都说python多进程更快,因为GIL(全局解释器锁).但是我在写代码的时候,测试时间却是多线程更快,所以这到底是怎么回事?最近再做分词工作,原来的代码速度太慢,想提速,所以来探求一下有效方法(文末有代码和效果图) 这里先来一张程序的结果图,说明线程和进程谁更快 一些定义 并行是指两个或者多个事件在同一时刻发生.并发是指两个或多个

随机推荐