编程博客
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 发布
-
+
首页
05、并发编程 - 线程同步(二)
经过前面对线程同步初步了解,相信大家对线程同步已经有了整体概念,今天我们就来一起看看线程同步的具体方案。  # ***01***、ThreadStatic 严格意义上来说这两个并不是实现线程同步方案,而是解决多线程资源安全问题,而我们研究线程同步最终也是为了解决多线程资源安全问题,因此就先说下这两个用法。 ThreadStatic 特性可以实现线程本地存储,使得每个线程都有一个独立的字段副本。从而避免不同线程间共享资源。 使用 ThreadStatic 时需要注意以下几点: 1、ThreadStatic 仅能作用于静态字段;。 2、ThreadStatic 字段不应使用内联初始化。 3、每个线程都会有独立的\_threadLocalVariable 实例,当线程退出时,相关的线程本地存储会被清除。 4、由于 ThreadStatic 是线程局部存储,它并不是跨线程共享数据的解决方案。 使用起来也很简单,我们来着重说说上面注意点的第二点,虽然语法上可以写出内联初始化,但是这样会导致一个问题:仅有访问其的首个线程上可以获取其初始化变量值,而其他所有线程都只能获取到变量类型的默认值。比如下面这段代码: ```csharp [ThreadStatic] public static int _threadStaticValue = 1; public static void ThreadStaticRun() { var thread1 = new Thread(ThreadStatic1); var thread2 = new Thread(ThreadStatic2); var thread3 = new Thread(ThreadStatic3); thread1.Start(); thread2.Start(); thread3.Start(); } static void ThreadStatic1() { Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}"); } static void ThreadStatic2() { Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}"); } static void ThreadStatic3() { Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}"); } ``` 也就是上面代码只有一个线程能打印出 1,其他线程都只能打印出 0,我们看看实际打印结果:  因此注意项第二点提出 ThreadStatic 字段不应使用内联初始化,因为这样并不能保证每个线程都能获取到相同的初始值。 也因为 ThreadStatic 有这个缺陷所以引出了 ThreadLocal。 # ***02***、ThreadLocal 可以说 ThreadLocal 功能和 ThreadStatic 完全一样,并且还解决了其缺陷,因此更推荐使用 ThreadLocal。 可以使用 System.Threading.ThreadLocal 类型创建一个基于实例的线程本地变量,该变量由你提供的 Action 委托在所有线程上进行初始化。如下示例中,访问\_threadLocalValue 的所有线程都可以获取到初始化值 1。 ```csharp private static ThreadLocal<int> _threadLocalValue = new ThreadLocal<int>(() => 1); public static void ThreadLocalRun() { var thread1 = new Thread(ThreadLocal1); var thread2 = new Thread(ThreadLocal2); var thread3 = new Thread(ThreadLocal3); thread1.Start(); thread2.Start(); thread3.Start(); } static void ThreadLocal1() { Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue.Value}"); } static void ThreadLocal2() { Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue.Value}"); } static void ThreadLocal3() { Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue.Value}"); } ``` 执行结果如下:  并且可以通过 ThreadLocal.Value 属性进行读取和写入,也就是通过\_threadLocalValue.Value 对变量进行赋值和取值。 # ***03***、volatile 关键字 首先 volatile 关键字同样不是一个完整的线程同步机制,其主要作用是防止缓存和防止编译器优化。 在 C#语言开发中,由于编译器优化、JIT 编译、硬件缓存以及内存重排序等行为,很容易使得程序出现并发错误,尤其在多线程环境下这些情况会更为明显。虽然这些优化是在不影响程序逻辑的情况下进行的,但是因为重新排序对内存的读取和写入,进而可能导致数据竞争和同步问题。 volatile 关键字就是为了告诉编译器和运行时:该字段的值可能会被多个线程同时修改,因此每次访问该字段时,都应该直接从主内存中读取,而不是使用寄存器或缓存中的值。这样可以防止 CPU 的优化行为导致某些线程读取到过时的值。 我们一起看看如下代码: ```csharp //控制线程的标志 private static bool _flag = false; //计数器 private static int _counter = 0; public static void VolatileRun() { var thread1 = new Thread(Volatile1); var thread2 = new Thread(Volatile2); thread1.Start(); thread2.Start(); thread1.Join(); thread2.Join(); //Console.WriteLine($"计数器最后的值: {counter}"); } static void Volatile1() { //注意:以下两行代码可能按相反的顺序执行 //设置计数器 _counter = 88; //线程1:设置标志位,并且增加计数器 _flag = true; } static void Volatile2() { //注意:_counter可能优先于_flag读取 //线程2:等待标志位变为 true,然后读取计数器 //等待 _flag 被设置为 true while (!_flag) ; //打印计数器值 Console.WriteLine($"当前计数器的值: {_counter}"); } ``` 上面的代码很难在复现下面要说的问题,因此下面仅以此代码作为示例讲解。 上面代码的问题在于,经过编译器优化和内存重排序后, Volatile1 线程中的两行赋值代码可能被颠倒了顺序,如果从单线程角度来说这个顺序颠倒无关紧要,最总结果都是\_counter 被赋值了 88,\_flag 被赋值了 true。但是在多线程环境下,对于 Volatile2 线程来说就完全不一样了,此时却先读取到\_flag 为 true,然后打印\_counter 为 0,和预期完全不一样。 我们再从另一个角度来说,假定 Volatile1 线程中的代码安装编码顺序执行了,没有被优化。在编译 Volatile2 线程中的代码时,编译器必须生成代码将\_flag 和\_counter 从 RAM(主存)中读入 CPU 寄存器,此时 RAM 可能先读入\_counter 的值,为 0。与此同时 Volatile1 线程可能执行,将\_counter 修改为 88,想\_flag 修改为 true。此时 Volatile2 线程的 CPU 寄存器还没有看到\_counter 已被 Volatile1 线程修改为 88,然后继续将\_flag 的值从 RAM 中读入 CPU 寄存器,但是由于此时\_flag 已经被 Volatile1 线程修改为 true,所以最后 Volatile2 线程同样会打印\_counter 为 0。 开发时很容易忽略这些细微之处,并且由于开发调试环境不会进行代码优化,就导致问题往往到了生产环境下才显现出来。 为了解决这个问题我们就可以使用 volatile 关键字了。对于被声明为 volatile 的字段将从编译器优化、JIT 编译、硬件缓存以及内存重排序等优化中排除,使用也很简单,可以如下使用: ```csharp private static volatile bool _flag = false; ``` 另外 volatile 关键字不能引用于 double,long,数组等类型,可以使用 Volatile.Read 和 Volatile.Write 静态方法来完成。 同时 volatile 关键字虽然可以解决许多并发问题,但是因为其不是原子操作,因此它并不能算是一个完整的线程同步机制,因此在多线程环境下还是需要借助一些其他同步机制来保证线程安全。 因此 volatile 最大的应用场景就是在需要保证多个线程访问同一个共享变量时,大家都可以立刻看到最新的值,尤其是不涉及复杂操作如递增递减等。
个人天使
2025年2月10日 10:23
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码