【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。


目录
相关文章
|
设计模式 XML 算法
策略模式(Strategy Pattern)深度解析教程
策略模式属于行为型设计模式,通过定义算法族并将其封装为独立的策略类,使得算法可以动态切换且与使用它的客户端解耦。该模式通过组合替代继承,符合开闭原则(对扩展开放,对修改关闭)。
|
域名解析 弹性计算 负载均衡
新手上云教程参考:阿里云服务器租用、域名注册、备案及域名解析流程图文教程
对于想要在阿里云上搭建网站或应用的用户来说,购买阿里云服务器和注册域名,绑定以及备案的流程至关重要。本文将以图文形式为您介绍阿里云服务器购买、域名注册、备案及绑定的全流程,以供参考,帮助用户轻松上手。
|
存储 弹性计算 人工智能
阿里云发票申请图文教程及常见问题解析
在购买完阿里云服务器或者其他云产品之后,如何申请发票成为了许多用户关注的焦点。尤其是对于初次购买阿里云服务器的用户来说,发票申请流程可能并不熟悉。本文将为大家详细介绍阿里云服务器购买之后如何申请发票,以及申请过程中可能遇到的常见问题,帮助大家轻松完成发票申请。
|
弹性计算 运维 网络安全
阿里云轻量应用服务器产品解析与搭建个人博客网站教程参考
轻量应用服务器(Simple Application Server)作为阿里云面向单机应用场景推出的云服务器产品,以其一键部署、一站式管理、高性价比等特性,深受个人开发者、中小企业及入门级用户的喜爱。本文将全面解析阿里云轻量应用服务器的产品优势、应用场景、使用须知,以及使用轻量应用服务器搭建个人博客网站的详细教程,帮助用户更好地了解和使用这一产品。
|
算法 调度 开发者
多线程编程核心:上下文切换深度解析
在多线程编程中,上下文切换是一个至关重要的概念,它直接影响到程序的性能和响应速度。本文将深入探讨上下文切换的含义、原因、影响以及如何优化,帮助你在工作和学习中更好地理解和应用多线程技术。
402 4
|
缓存 Java 调度
多线程编程核心:上下文切换深度解析
在现代计算机系统中,多线程编程已成为提高程序性能和响应速度的关键技术。然而,多线程编程中一个不可避免的概念就是上下文切换(Context Switching)。本文将深入探讨上下文切换的概念、原因、影响以及优化策略,帮助你在工作和学习中深入理解这一技术干货。
370 10
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
255 6
|
JSON API 数据格式
二维码操作[二维码解析基础版]免费API接口教程
此接口用于解析标准二维码内容,支持通过BASE64编码或远程图片路径提交图片。请求需包含用户ID、用户KEY、图片方式及图片地址等参数,支持POST和GET方式。返回结果包括状态码和消息内容,适用于图片元素简单的二维码解析。
675 2
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####

推荐镜像

更多
  • DNS
  • 下一篇
    开通oss服务