编程博客
002、修改Nuget包位置
001、C#常用的单词
003、收藏的书签
回家准备
004、ASP.NET Core 3.0 gRPC
001、ASP.NET Core 3.0 使用gRPC
002、ASP.NET Core 3.0 gRPC 双向流
003、ASP.NET Core 3.0 gRPC 身份认证和授权
005、飞牛NAS
001、 FnOS飞牛系统实用配置1-应用远程访问(迅雷)
006、DeepSeek
001、DeepSeek 使用指南
007、并发编程
01、并发编程 - 死锁的产生、排查与解决方案
02、并发编程 - 初识线程
03、并发编程 - 线程浅试
04、并发编程 - 线程同步(一)
05、并发编程 - 线程同步(二)
06、并发编程 - 线程同步(三)之原子操作 Interlocked 简介
07、并发编程 - 线程同步(四)之原子操作 Interlocked 详解一
08、并发编程 - 线程同步(五)之原子操作 Interlocked 详解二
09、并发编程 - 线程同步(六)之锁 lock
008、启动项
03、应用--Program 中的 WebApplication
02、主机--Host
01、Program 文件的作用
04、控制反转 IOC 与依赖注入 DI
05、中间件
06、Logger 原理及配置 Log4Net
07、ElasticSearch
本文档使用 MrDoc 发布
-
+
首页
04、并发编程 - 线程同步(一)
经过前面对线程的尝试使用,我们对线程的了解又进一步加深了。今天我们继续来深入学习线程的新知识 —— 线程同步。  # ***01***、什么是线程同步 线程同步是指在多线程环境下,确保多个线程在同时使用共享资源时不会发生冲突或数据不一致问题的技术,保证线程间的正确协作。它的目的是使得多个线程在执行过程中能够按照某种顺序、安全地使用共享资源。 # ***02***、为何需要线程同步 ## 1、避免竞争条件 不知道大家还记得在《并发编程 - 初识线程》中出现的关键字 volatile 和特性 ThreadStatic 吗?它们都是为了解决多线程共享资源问题。 在多线程中当多个线程需要同时使用共享资源时,很容易产生互相竞争资源使用权的情况,这一问题也叫竞争条件。此时就可以通过线程同步技术实现多个线程按顺序使用共享资源,从而避免竞争条件。 ## 2、保证共享资源安全 我们举个简单的例子,假如我的银行账户里有 1000 元,此时我正在用电子银行在线上操作准备向我老婆的账户里转账 100 元,而恰巧此时我老婆拿着我的银行卡准备取款 500。 假如银行系统还是一个只有多线程,没有线程同步功能的老系统,在这一前置条件下。假如恰巧我们俩在同一瞬间点了确认操作,相信此时系统会发生什么? 有可能会是系统同时收到我们俩的请求,此时我的操作线程 A,首先读取我账户余额 1000,然后执行转账操作把余额减 100 得到 900,再更新至余额中。而我老婆的操作线程 B 因为是和我同时的,所以在读取我账户余额的时候得到的也是 1000,而不是 900,此时线程 B 执行取款 500 操作把余额减 500 得到 500,再更新至余额中。 可以发现我们俩最后更新余额,无论谁更新成功最后结果都是不正确的。这个例子就导致银行账户余额最终不正确,也就是我们说的共享资源不安全。如果使用线程同步,使得线程 A、B 可以按顺序执行,无论谁先执行最终结果都会是正确的。 下面我们再来结合代码举一个经典问题 —— **torn read**。 先解释一下什么叫 torn read,可以翻译成一次读取被撕成两半。或者说在机器级别上,要分两个 MOV 指令才能读完。 具体来说就是一个 long 类型变量\_var,当一个线程把\_var 赋值为 0x0123456789ABCDEF,而此时另一个线程来读取\_var,结果读取的值是 0x0123456700000000 或 0x0000000089ABCDEF。这同样是因为多线程导致的共享资源不安全问题。 下面看看模拟代码实现效果: ```csharp public class ThreadSync { //共享的int64变量 public static long _var; public static void Run() { //启动写入线程 var writerThread = new Thread(WriteToSharedValue); //启动读取线程 var readerThread = new Thread(ReadFromSharedValue); //启动线程 writerThread.Start(); readerThread.Start(); //等待线程执行完成 writerThread.Join(); readerThread.Join(); } //写入线程 static void WriteToSharedValue() { //模拟分两步写入 long high = 0x01234567; long low = 0x89ABCDEF; unsafe { //将 _var 分成高低两部分写入 //写高 32 位 _var = high << 32; // 确保读取线程能在这里读取中间值 Thread.Sleep(0); //写低 32 位 _var |= low; } Console.WriteLine($"写: 写入值 0x{_var:X16}"); } //读取线程 static void ReadFromSharedValue() { // 读取共享变量的值 Console.WriteLine($"读: 读取值 0x{_var:X16}"); } } ``` 我们看下执行效果:  当然上面的例子并不是每次都会出现的,可能需要多运行几次,另外关于写入线程为什么不是直接赋值而是把值拆成高低位分两次写入? 这是因为我的电脑是 64 位系统,在大多数现代的 x64 系统架构(例如 Intel 和 AMD 处理器)上,64 位的原子性操作通常是被保证的。即使对于像 long(64 位)这种数据类型,处理器通常会在硬件层面确保它的读写操作是原子性的,因此,不太容易发生撕裂的读(torn read)。 所以这里的代码把一次赋值行为认为拆解成两步,同时 Thread.Sleep(0)也为了让当前线程主动让出 CPU 时间片,使读线程有机会读取,使其更贴近在 x32 环境下运行的情况。如果有条件可以用直接赋值再 x32 环境下看看效果。 # ***03***、如何实现线程同步 ## 1、避免资源共享 当然严格意义上说可能这一条不算是线程同步,只能说解决了多线程碰到的问题,达到线程同步的效果。 如果没有共享资源,那么自然就无须进行线程同步。大多数时候可以通过重新设计程序来除移共享状态,从而去掉复杂的同步构造。尽可能避免在多个线程间使用单一对象。 除了通过重新设计来移除共享状态,还可以通过语言特性设计使其达到无共享状态。比如值类型在传递过程中总是被复制,每个线程都会有自己的数据副本,比如看下面这个方法: ```csharp public static int Max(int val1, int val2) { return val1 > val2 ? val1 : val2; } ``` 即使这个方法没有使用任何线程同步方法,这个方法也是线程安全的。因为值类型特性原因,所以传给 Max 的两个 int 值会复制到方法内部,形成自己的数据副本。此时无论有多少个线程调用 Max 方法,每个线程处理的都是它自己的数据,线程之间并不会互相干扰。 ## 2、用户模式同步机制 用户模式同步机制指在用户空间内完成线程的阻塞和唤醒操作,由程序自己管理同步对象的一种同步方式,因为不涉及与操作系统内核交换,因此开销较低,更轻量级。 实现方式有 SpinLock、SpinWait、Monitor(lock)等。  ## 3、内核模式同步机制 内核模式同步机制是指在操作系统内核空间就完成线程的挂起与恢复,由操作系统管理同步对象的一种同步方式,因为每次线程同步操作都需要操作系统参与,因此必然回涉及内核态的上下文切换,同时还是涉及到操作系统内部的数据结构和资源管理,因此内核模式同步机制往往会导致较高的开销。 实现方式有 Semaphore、Mutex、AutoResetEvent 等。 ## 4、混合模式同步机制 混合模式同步机制在某些情况下会根据线程竞争的情况在用户模式和内核模式之间切换。通常,当资源访问冲突较小或线程阻塞较少时,采用用户模式同步;当资源争用较多或有较大的线程等待时,自动切换到内核模式同步。 实现方式有 SemaphoreSlim、ManualResetEventSlim、CountDownEvent、Barrier、ReaderWriterLockSlim 等。
个人天使
2025年2月10日 10:20
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码