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

简介: 【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。


目录
相关文章
|
8月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
6月前
|
存储 监控 算法
基于 C++ 哈希表算法的局域网如何监控电脑技术解析
当代数字化办公与生活环境中,局域网的广泛应用极大地提升了信息交互的效率与便捷性。然而,出于网络安全管理、资源合理分配以及合规性要求等多方面的考量,对局域网内计算机进行有效监控成为一项至关重要的任务。实现局域网内计算机监控,涉及多种数据结构与算法的运用。本文聚焦于 C++ 编程语言中的哈希表算法,深入探讨其在局域网计算机监控场景中的应用,并通过详尽的代码示例进行阐释。
119 4
|
8月前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
8月前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
10月前
|
存储 算法 安全
基于红黑树的局域网上网行为控制C++ 算法解析
在当今网络环境中,局域网上网行为控制对企业和学校至关重要。本文探讨了一种基于红黑树数据结构的高效算法,用于管理用户的上网行为,如IP地址、上网时长、访问网站类别和流量使用情况。通过红黑树的自平衡特性,确保了高效的查找、插入和删除操作。文中提供了C++代码示例,展示了如何实现该算法,并强调其在网络管理中的应用价值。
|
10月前
|
Java
【JavaEE】——多线程常用类
Callable的call方法,FutureTask类,ReentrantLock可重入锁和对比,Semaphore信号量(PV操作)CountDownLatch锁存器,
|
10月前
|
Java 程序员 调度
【JavaEE】线程创建和终止,Thread类方法,变量捕获(7000字长文)
创建线程的五种方式,Thread常见方法(守护进程.setDaemon() ,isAlive),start和run方法的区别,如何提前终止一个线程,标志位,isinterrupted,变量捕获
|
7月前
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
658 29
|
7月前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
189 4
|
7月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~