多线程和锁和原子操作和内存栅栏(二)

本文详细介绍了多线程环境中常见的锁机制,包括内核锁、自旋锁、混合锁等,探讨了它们的工作原理、应用场景及优缺点,并对比了Mutex、Semaphore、Monitor等多种锁的具体实现。

        这里记录下各种锁的使用和使用场景,在多线程场景开发时,我们经常遇到多个线程同时读写一块资源争抢一块资源的情况,比如同时读写同一个字段属性,同时对某个集合进行增删改查,同时对数据库进行读写(这里要用到事物,数据库锁等知识)等,这里往往为了做到数据的一致性和有效性,需要进行对线程之间的在某一时刻或一小段时间的同步操作,来保证数据安全,不会中途造成数据值读写不准确的情况,这个时候一般情况下会用到锁,锁的作用就是为了在某一时刻做线程同步而准备的,但是锁的使用不宜过多,因为本就是多线程场景,而使用多线程就意味着程序是为了做弹性伸缩准备的,线程同步不可避免的会对程序造成性能影响,所以解决多线程之间资源冲突问题最好的方式,就是从一开始就想办法不冲突。

        并发说的是程序在某一时刻被同时请求,而并行指的是程序在运行时的某一时段持续多线程执行任务。

        锁的类型有内核锁,自旋锁,混合锁,

内核锁 是一种基于内核代码的锁,这种锁是一种线程阻塞机制,阻塞的好处是线程会执行到锁阻塞的位置,让线程不继续执行却不消耗CPU计算资源,从而实现线程同步,但不好的一点是,阻塞时,CLR会把C#托管代码转换成非托管代码,然后从非托管代码转换成内核代码,释放时倒序转换回去,这本身就消耗了一定的CPU资源,速度会稍慢,并且在大并发场景下,大量的锁释放导致大量线程同时处于活动状态,使得系统忙于进行线程上下文切换,而不做该做的事情,问题严重

自旋锁 是一种基于内部循环的锁,循环时会不断判断条件,以便于在某一时刻能够退出自旋锁,这种锁的应用场景一般是在并发量比较小,且执行逻辑并不耗时,执行时间比较短的情况下才会使用,自旋锁在锁住期间,由于内部是通过循环来实现线程不继续执行,所以仍然会占用CPU计算资源,这在并发量高的场景下非常不适合使用,大概率导致计算机卡死。

混合锁 是一种内核锁+自旋锁的混合使用,因为单纯使用内核锁或者自旋锁效率都不高,所以尝试把两种技术相结合的方式取长补短,实现锁性能的优化。

 

Mutex 锁是一种进程同步锁,表示多个进程之间可以共享同一个锁,进程之间通过字符串 获取同一个锁实现进程之间的同步。

通过反编译工具可知,

 

由于它的内部实现是通过非托管代码,无法查看具体实现,暂时没深入研究。

 

Semaphore 锁提供了释放锁的次数,构造参数中 initialCount,和maximumCount 指的是初始可释放次数和最大可释放次数,并且提供了进程之间同步的name参数,这和Mutex锁是一样的。

它继承的是WaitHandle ,所以WaitOne 方法是父类的,反编译时可以看到是调用了非托管代码,所以看不到具体实现,

Realease 方法反编译时也是调用了非托管代码,

,所以暂时没深入研究。

SemaphoreSlim 锁是对Semaphore锁的一种简单实现,保留部分Semaphore锁的特点,它是一种混合锁,通过自旋加阻塞的形式,优化纯自旋或阻塞锁的弊端,减少锁的资源消耗,并提供了多种更方便使用的api。

 

 

由反编译可知,释放时也是通过阻塞锁的释放方法进行释放

AutoResetEvent 锁是父类EventWaitHandle的一个派生类,只是在实例化时指定了父类构造函数的参数值,

ManualResetEvent 同上,

EventWaitHandle 锁是一个信号锁,而信号量就是锁内部维护的一个bool值或其他可用来表示状态的值。

Set方法的内部实现还是通过非托管代码实现

如之前的方式使用ManualResetEvent 时,锁出现了不一样的结果

ManualResetEvent 锁会在锁是可用时也就是构造传入true时,WaitOne方法不会等待,因为信号量已经是true了,这个锁在信号量设为true时默认会释放所有的阻塞,而AutoResetEvent 锁是单一释放,这在初始化时由父类构造参数决定。它可以改为以下形式使用,在一开始设置信号量为false,让每个线程都阻塞,知道某一刻调用了Set方法,修改信号量,可以同时放开所有线程阻塞,只不过现在无法保证数据的有效性了,因为阻塞放开后,并没有对目标值做原子操作。

 

ManualResetEventSlim 锁是ManualResetEvent的简易实现,是一种混合锁,内部通过自旋(这里是用for循环)+阻塞的形式,使用方式和ManualResetEvent 一样,

锁一旦放开终止是没用的,因为在后续的执行中并没有等待锁。

Monitor锁 是一个阻塞锁 ,也是一个静态类,他提供最基本的线程阻塞操作,前面有几种锁的阻塞和释放都是通过这个静态类来完成的,它提供两种释放方法Pulse 和 PulseAll 方法来释放单个或者多个锁,且必须用lock关键字或者在Enter 和Exit方法之间进行等待和释放操作。Enter 和Ext 其实就是lock关键字在运行时执行的方法。

释放时并不会按照阻塞时的顺序释放。

CountdownEvent 锁,是一个混合锁,自旋加阻塞的方式,锁内部维护一个ManualResetEventSlim锁变量,是一个代理锁,提供一个int变量,当变量的数值归零时,取消所有线程的等待,利用reset(0),强制释放所有的锁,源码中的实现如下图:

具体使用方式:由于是同时释放,所以结果并不准确。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

<think>我们被要求比较`std::lock_guard``std::atomic`,并解释它们的区别应用场景。 首先,我们需要明确这两者属于不同的并发编程机制,解决的问题不同。 1. `std::lock_guard`: - 属于互斥(mutex)机制的一部分,用于保护临界区。 - 它通过RAII(资源获取即初始化)方式管理互斥,在构造时加,在析构时解,确保不会忘记解。 - 使用`std::lock_guard`时,同一时间只有一个线程可以进入被保护的代码段。 - 适用于需要保护一段代码(临界区)的情况,特别是当多个线程需要访问共享资源(如一个数据结构)时,避免数据竞争。 2. `std::atomic`: - 提供原子操作,确保对特定变量的操作原子的(不会被中断)。 - 不需要使用互斥,因此通常比互斥更高效(尤其在简单操作如读取、写入、递增等)。 - 支持多种内存顺序(memory order),允许程序员在性能同步需求之间进行权衡。 - 适用于单个变量(或简单数据结构)的原子操作,例如标志位、计数器等。 区别总结: - 作用范围:`std::lock_guard`保护一段代码(临界区),而`std::atomic`保护单个变量。 - 性能:`std::atomic`通常比互斥(如`std::lock_guard`使用的)更高效,因为它不需要进入内核态(在无竞争时)且无阻塞。 - 复杂性:`std::atomic`适用于简单操作,而`std::lock_guard`可以保护复杂的操作序列(如多个变量的修改,或调用多个函数)。 应用场景: - `std::lock_guard`:当需要保护一个代码块(如修改多个共享变量,或者执行一系列操作)时使用。例如: std::mutex mtx; { std::lock_guard<std::mutex> lock(mtx); // 修改多个共享变量 shared_var1 = ...; shared_var2 = ...; } - `std::atomic`:当只需要对一个变量进行原子操作(如计数器、标志位)时使用。例如: std::atomic<int> counter(0); // 在多个线程中安全地递增 counter.fetch_add(1, std::memory_order_relaxed); 另外,我们还可以考虑使用`std::atomic`实现无编程,但无编程通常更复杂,容易出错,只在性能关键且简单操作时使用。 注意:`std::atomic`不能替代互斥。当需要保护一个操作序列(非原子操作)时,必须使用互斥。 参考引用: [^1] 提到了`std::mutex``std::lock_guard`(虽然未直接提到`lock_guard`,但`lock_guard`是管理`mutex`的RAII包装)。 [^2][^4] 提到了原子操作内存顺序,说明`std::atomic`可以配合不同的内存顺序使用。 [^3] 给出了`std::atomic_bool`的使用场景,如标志位。 因此,我们可以这样组织回答: 标题:C++中std::lock_guard与std::atomic的区别与应用场景 内容: 1. 概述 2. 核心区别(表格形式) 3. 详细说明各自的工作原理适用场景 4. 代码示例 5. 总结(何时使用哪种) 生成相关问题。</think>### C++ 中 `std::lock_guard` 与 `std::atomic` 的区别与应用场景 #### 1. **核心区别** | 特性 | `std::lock_guard` | `std::atomic` | |--------------------------|------------------------------------|-----------------------------------| | **同步机制** | 基于互斥(Mutex)的阻塞同步 | 无(Lock-free)原子操作 | | **保护粒度** | 保护代码块(临界区) | 保护单个变量 | | **性能开销** | 较高(涉及内核态切换) | 较低(硬件级原子指令) | | **重排序限制** | 隐式全内存屏障(`std::memory_order_seq_cst`) | 可定制内存顺序(如 `memory_order_release`) | | **适用场景** | 复杂操作(多变量修改/函数调用) | 简单操作(标志位/计数器) | #### 2. **工作原理对比** **`std::lock_guard`(互斥)** ```cpp std::mutex mtx; int shared_data = 0; void safe_increment() { std::lock_guard<std::mutex> lock(mtx); // 自动加 shared_data++; // 临界区操作 } // 自动解(析构时) ``` - 通过 RAII 管理互斥,确保临界区独占访问 - 隐式建立全内存屏障(顺序一致性)[^1] **`std::atomic`(原子操作)** ```cpp std::atomic<int> counter(0); void fast_increment() { counter.fetch_add(1, std::memory_order_relaxed); // 无原子操作 } ``` - 直接使用 CPU 原子指令(如 x86 的 `LOCK XADD`) - 支持精细内存顺序控制(如 `memory_order_acquire`/`release`)[^2][^4] #### 3. **典型应用场景** | **场景** | 推荐方案 | 原因说明 | |-------------------------|----------------------|--------------------------------------------------------------------------| | **复杂数据结构修改** | `std::lock_guard` | 需保护多个关联变量(如链表节点修改)[^1] | | **简单标志位控制** | `std::atomic<bool>` | 低开销(如 `kill_switch` 终止线程)[^3] | | **高并发计数器** | `std::atomic<int>` | 无设计避免线程阻塞(如访问统计) | | **条件变量同步** | `std::lock_guard` + `std::condition_variable` | 需配合互斥保证唤醒逻辑正确性 | | **跨线程数据发布** | `std::atomic` + Release/Acquire | 通过内存栅栏确保可见性(如 `ready_flag.store(true, std::memory_order_release)`)[^2] | #### 4. **性能关键差异** - **原子操作优势** - 无设计:平均延迟 $O(1)$(无线程切换) - 吞吐量高:适合高频简单操作(如计数器递增) - **互斥劣势** - 系统调用开销:线程阻塞/唤醒成本约 $1-10\mu s$ - 优先级反转风险:可能引发死 > ⚠️ 经验法则: > - 当操作涉及 **多个变量** 或 **非原子指令序列** 时 → 用 `std::lock_guard` > - 当只需 **单个变量原子访问** 且 **高频调用** 时 → 用 `std::atomic` #### 5. **混合使用示例** ```cpp // 低延迟标志位 + 互斥保护复杂逻辑 std::atomic_bool data_ready(false); // 原子标志 std::mutex data_mtx; DataBuffer buffer; // 复杂数据结构 // 生产者线程 void producer() { { std::lock_guard<std::mutex> lock(data_mtx); buffer.update(); // 受保护复杂操作 } data_ready.store(true, std::memory_order_release); // 原子发布 } // 消费者线程 void consumer() { while (!data_ready.load(std::memory_order_acquire)); // 原子等待 std::lock_guard<std::mutex> lock(data_mtx); buffer.process(); // 安全访问 } ``` #### 6. **总结:如何选择** | **决策因素** | 选择 `std::lock_guard` | 选择 `std::atomic` | |------------------------|----------------------------------|----------------------------------| | **操作复杂度** | 多步骤非原子操作 | 单变量读/写/交换 | | **性能要求** | 非关键路径 | 高频调用热点区域 | | **内存顺序需求** | 需要强顺序保证 | 可接受宽松内存序(`relaxed`)[^4] | | **可维护性** | 逻辑直观不易出错 | 需深入理解内存模型 | > 💡 最佳实践:优先用 `std::atomic` 处理简单场景,仅在必要时升级到互斥。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值