【C++多线程同步】C++多线程同步和互斥的关键:std::mutex和相关类的全面使用教程与深度解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 【C++多线程同步】C++多线程同步和互斥的关键:std::mutex和相关类的全面使用教程与深度解析

Mutex 系列类(四种)


  • std::mutex,最基本的 Mutex 类

独占互斥量,只能加锁一次

std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。


std::mutex成员函数:

  • 构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。
  • lock(),调用线程将锁住该互斥量
  • unlock(), 解锁,释放对互斥量的所有权。
  • try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。

  • std::recursive_timed_mutex,递归 Mutex 类

递归的独占互斥量,允许同一个线程,同一个互斥量,多次被lock,用法和非递归的一样 跟windows的临界区是一样的,但是调用次数是有上限的,效率也低一些

和 std::mutex 不同的是,
std::recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,
std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同.


  • std::timed_mutex,定时 Mutex 类。

带超时的互斥量,独占互斥量 这个就是拿不到锁会等待一段儿时间,但是超过设定时间,就继续执行

std::time_mutex 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until()。

template< class Rep, class Period > bool try_lock_for( const
std::chrono::duration<Rep,Period>& timeout_duration ); 
//  (C++11 起) 
template< class Rep, class Period > bool try_lock_for( const
std::chrono::duration<Rep,Period>& timeout_duration ); 
//  (C++11 起)

  • try_lock_for:尝试锁定互斥,若互斥在指定的时限时期中不可用则返回。
  • try_lock_until:尝试锁定互斥,若直至抵达指定时间点互斥不可用则返回。

参数

timeout_duration - 要阻塞的最大时长

返回值

若成功获得锁则为 true ,否则为 false 。

异常

执行期间时钟、时间点或时长可能抛出的异常(标准库提供的时钟、时间点和时长决不抛出)


  • std::recursive_timed_mutex,定时递归 Mutex

带超时的,递归的,独占互斥量,允许同一个线程,同一个互斥量,多次被lock,用法和非递归的一样


Lock 类(两种)


  • std::lock_guard,与 Mutex RAII 相关,方便线程对互斥量上锁。
  • std::lock_guard在构造时自动锁定其管理的mutex对象,实现了std::mutex的lock功能,而在析构时则自动解锁,无须手动调用unlock,使得共享资源的访问更为安全。通过这种方式,它可以有效地保护共享数据,防止并发访问导致的数据不一致。
  • std::lock_guard为了防止在线程使用mutex加锁后由于异常退出导致死锁的问题,建议使用lock_guard代替mutex。这样,在某个lock_guard对象的生命周期内,它所管理的锁对象会一直保持上锁状态;而lock_guard的生命周期结束之后,它所管理的锁对象会被解锁。这一特性可以简化编程,使开发者不必担心异常或忘记解锁导致的问题。
  • std::lock_guard类的构造函数禁用拷贝构造,且禁用移动构造。std::lock_guard类除了构造函数和析构函数外没有其它成员函数。这种设计意图是禁止对象的复制和移动,确保锁的唯一性和安全性。这一特性也使得std::lock_guard只能被锁定一次,并在销毁时自动解锁。
  • 因为std::lock_guard对象在创建时就锁定了mutex对象,并且在其生命周期结束时自动解锁,这使得它特别适合用于函数和小范围的代码块中。在这些场景下,当代码执行完毕或遇到return、throw等情况时,std::lock_guard对象的生命周期结束,从而自动释放锁,大大增强了代码的健壮性。

注意,lock_guard对象并不负责管理mutex对象的生命周期。互斥对象的生存期至少要延长到锁它的lock_guard被销毁。这一点对于正确使用std::lock_guard至关重要。它只负责锁的获取和释放,对于互斥量对象本身的创建和销毁并不负责。这样可以更加灵活地管理互斥量的生命周期。


  • std::unique_lock,与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。
  • unique_lock是个类模板,工作中,一般lock_guard(推荐使用);lock_guard取代了mutex的lock()和unlock();
  • unique_locklock_guard灵活很多,效率上差一点,内存占用多一点。
  • std::unique_lock 可以在构造时传递第二个参数用于管理互斥量,且能传递不同域中互斥量所有权。
  • unique_lock私有成员为指针 _Mutex /*_Pmtx,指向传递进来的互斥量,lock_guard私有成员为引用_Mutex& _MyMutex,引用传递进的互斥量。这就决定了unique_lock能够实现传递互斥量的功能。
  • unique_lock的用法比较多,如果对锁的需求比较简单推荐使用lock_guard。当需要超时或者手动解锁等功能,可以考虑使用unique_lock

不同的情况可使用对应的构造创建对象


unique_lock(mutex, adopt_lock_t) //传递被使用过的mutex,且已经被上过锁,通过。无上锁动作,不阻塞。

unique_lock(mutex, defer_lock_t) //传递被使用过的mutex,未被上过锁。无上锁动作,不阻塞。

unique_lock(mutex, try_to_lock_t) //任何状态的mutex。尝试上锁,不阻塞。

unique_lock(_Mutex& _Mtx, const chrono::duration<_Rep, _Period>&
_Rel_time) //在指定时间长内尝试获取传递的mutex的锁返回。若无法获取锁,会阻塞到指定时间长。

unique_lock(mutex_type& m,std::chrono::time_point<Clock,Duration>
const& absolute_time) //在给定时间点尝试获取传递的mutex锁返回。若无法获取锁,会阻塞到指定时间点。

unique_lock(unique_lock&& _Other)
//将已经创建的unique_lock锁的所有权转移到新的锁。保持之前锁的状态,不阻塞 ```

unique_lock的成员函数

unique_lock除了拥有跟std::mutex一样的三个成员函数意外,还提供release()函数。
release()返回它所管理的mutex对象指针,并释放所有权;也就是说,这个unique_lock和mutex不再有关系。严格区分unlock()与release()的区别,不要混淆。

      std::unique_lock<std::mutex> sbguard(my_mutex);
      std::mutex *ptx = sbguard.release(); //现在你有责任自己解锁了
      msgRecvQueue.push_back(i);
      ptx->unlock(); //自己负责mutex的unlock了

unique_lock所有权的传递

std::unique_lockstd::mutex sbguard(my_mutex);//所有权概念
std::unique_lockstd::mutex sbguard1(my_mutex);
std::unique_lockstd::mutex sbguard2(sbguard1);//此句是非法的,复制所有权是非法
//sbguard拥有my_mutex的所有权;sbguard可以把自己对mutex(my_mutex)的所有权转移给其他的unique_lock对象;
//所以unique_lock对象这个mutex的所有权是可以转移,但是不能复制。

所有权的传递的方法

方法1 :std::move()

方法2:return std:: unique_lockstd::mutex


其他类型


  • std::once_flag(提供一种方式,可以保证其实例绑定的函数,能且仅能被执行一次)

std::once_flag是一个用于与std::call_once函数配合使用的标志类型。它可以确保一个函数在多线程环境中仅被调用一次。这在某些情况下非常有用,例如在单例模式中或在初始化全局资源时。

std::once_flag本身没有任何成员函数或公共接口。它仅仅是一个标识,用于记录一个给定的函数是否已经被调用。std::once_flag的一个实例应该与需要仅执行一次的函数绑定。为了防止多个线程竞争执行这个函数,您可以将std::once_flag与std::call_once一起使用。

  • adopt_lock_t(假设调用方线程已拥有互斥的所有权)
  • Value used as possible argument to the constructor of unique_lock or lock_guard.
  • objects constructed with adopt_lock do not lock the mutex object on construction, assuming instead that it is already locked by the current thread.
  • The value is a compile-time constant that carries no state, and is merely used to disambiguate between constructor signatures.
  • adopt_lock_t is an empty class.
  • std::defer_lock_t(不获得互斥的所有权)
  • Value used as possible argument to unique_lock’s constructor.
  • unique_lock objects constructed with defer_lock do not lock the mutex object automatically on construction, initializing them as not owning a lock.
  • The value is a compile-time constant that carries no state, and is merely used to disambiguate between constructor signatures.
  • defer_lock_t is an empty class.
  • std::try_to_lock_t(尝试获得互斥的所有权而不阻塞)
  • Value used as possible argument to unique_lock’s constructor.
  • unique_lock objects constructed with try_to_lock attempt to lock the mutex object by calling its try_lock member instead of its lock member.
  • The value is a compile-time constant that carries no state, and is merely used to disambiguate between constructor signatures.

相关函数


std::try_lock,尝试同时对多个互斥量上锁。

std::lock,可以同时对多个互斥量上锁。

std::call_once,如果多个线程需要同时调用某个函数,call_once 可以保证多个线程对该函数只调用一次。

template <class Fn, class... Args> 
void call_once (once_flag&flag, Fn&& fn, Args&&... args);

flag:仅可执行一次的对象,其生命周期要比所在的线程长

Fn:执行函数

args:执行函数的可变参数

当多个线程使用同一个flag对象去调用函数call_once时,仅有一个线程可以真正的完成对函数Fn的调用,其他的线程会被阻塞,直到函数Fn返回.


综合示例

#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
#include <chrono>
#include <condition_variable>
#include <functional>

class TaskQueue {
public:
    void push(int task) {
        {
            std::unique_lock<std::mutex> lock(mutex_);
            tasks_.push_back(task);
        }
        cond_.notify_one();
    }

    int pop() {
        std::unique_lock<std::mutex> lock(mutex_);
        cond_.wait(lock, [this] { return !tasks_.empty(); });
        int task = tasks_.front();
        tasks_.erase(tasks_.begin());
        return task;
    }

private:
    std::vector<int> tasks_;
    std::mutex mutex_;
    std::condition_variable cond_;
};

std::once_flag flag;

void run_once() {
    std::cout << "Run once." << std::endl;
}

void worker(TaskQueue& taskQueue) {
    std::call_once(flag, run_once);

    while (true) {
        int task = taskQueue.pop();

        if (task == -1) {
            break;
        }

        std::cout << "Processing task: " << task << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main() {
    TaskQueue taskQueue;
    std::vector<std::thread> threads;

    for (int i = 0; i < 4; ++i) {
        threads.push_back(std::thread(worker, std::ref(taskQueue)));
    }

    for (int i = 0; i < 10; ++i) {
        taskQueue.push(i);
    }

    for (int i = 0; i < 4; ++i) {
        taskQueue.push(-1);
    }

    for (auto& thread : threads) {
        thread.join();
    }

    return 0;
}

在这个示例中,我们使用了以下组件:

std::mutex用于保护TaskQueue的内部数据结构。

std::unique_lock用于自动上锁和解锁互斥量。

std::condition_variable用于线程间的通知。

std::once_flag和std::call_once组合使用,确保run_once()函数只被执行一次。

注意,由于示例的简单性,我们没有使用到std::recursive_mutex、std::timed_mutex、std::recursive_timed_mutex和std::lock_guard。

这些类在不同的场景下可以用于替换示例中的std::mutex和std::unique_lock。


目录
相关文章
|
22天前
|
自然语言处理 编译器 Linux
|
11天前
|
API 数据安全/隐私保护
抖音视频,图集无水印直链解析免费API接口教程
该接口用于解析抖音视频和图集的无水印直链地址。请求地址为 `https://cn.apihz.cn/api/fun/douyin.php`,支持POST或GET请求。请求参数包括用户ID、用户KEY和视频或图集地址。返回参数包括状态码、信息提示、作者昵称、标题、视频地址、封面、图集和类型。示例请求和返回数据详见文档。
|
13天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
28天前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
|
30天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
30天前
|
Java
在Java多线程编程中,实现Runnable接口通常优于继承Thread类
【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
37 2
|
30天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
30 2
|
30天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
34 1
|
10天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
39 2
|
1月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
70 0
下一篇
无影云桌面