并发编程 - 线程同步(二)

打印 上一主题 下一主题

主题 887|帖子 887|积分 2661

经过前面对线程同步初步相识,信赖大家对线程同步已经有了整体概念,本日我们就来一起看看线程同步的具体方案。

01、ThreadStatic

严格意义上来说这两个并不是实现线程同步方案,而是解决多线程资源安全问题,而我们研究线程同步最终也是为相识决多线程资源安全问题,因此就先说下这两个用法。
ThreadStatic特性可以实现线程当地存储,使得每个线程都有一个独立的字段副本。从而制止差别线程间共享资源。
使用ThreadStatic时需要注意以下几点:
1、ThreadStatic仅能作用于静态字段;。
2、ThreadStatic字段不应使用内联初始化。
3、每个线程都会有独立的_threadLocalVariable实例,当线程退出时,相干的线程当地存储会被清除。
4、由于 ThreadStatic 是线程局部存储,它并不是跨线程共享数据的解决方案。
使用起来也很简单,我们来着重说说上面注意点的第二点,虽然语法上可以写出内联初始化,但是这样会导致一个问题:仅有访问其的首个线程上可以获取其初始化变量值,而其他所有线程都只能获取到变量类型的默认值。好比下面这段代码:
  1. [ThreadStatic]
  2. public static int _threadStaticValue = 1;
  3. public static void ThreadStaticRun()
  4. {
  5.     var thread1 = new Thread(ThreadStatic1);
  6.     var thread2 = new Thread(ThreadStatic2);
  7.     var thread3 = new Thread(ThreadStatic3);
  8.     thread1.Start();
  9.     thread2.Start();
  10.     thread3.Start();
  11. }
  12. static void ThreadStatic1()
  13. {
  14.     Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}");
  15. }
  16. static void ThreadStatic2()
  17. {
  18.     Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}");
  19. }
  20. static void ThreadStatic3()
  21. {
  22.     Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}");
  23. }
复制代码
也就是上面代码只有一个线程能打印出1,其他线程都只能打印出0,我们看看现实打印结果:

因此注意项第二点提出ThreadStatic字段不应使用内联初始化,由于这样并不能保证每个线程都能获取到雷同的初始值。
也由于ThreadStatic有这个缺陷以是引出了ThreadLocal。
02、ThreadLocal

可以说ThreadLocal功能和ThreadStatic完全一样,而且还解决了其缺陷,因此更推荐使用ThreadLocal。
可以使用 System.Threading.ThreadLocal 类型创建一个基于实例的线程当地变量,该变量由你提供的 Action 委托在所有线程上举行初始化。如下示例中,访问_threadLocalValue的所有线程都可以获取到初始化值1。
  1. private static ThreadLocal<int> _threadLocalValue = new ThreadLocal<int>(() => 1);
  2. public static void ThreadLocalRun()
  3. {
  4.     var thread1 = new Thread(ThreadLocal1);
  5.     var thread2 = new Thread(ThreadLocal2);
  6.     var thread3 = new Thread(ThreadLocal3);
  7.     thread1.Start();
  8.     thread2.Start();
  9.     thread3.Start();
  10. }
  11. static void ThreadLocal1()
  12. {
  13.     Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue.Value}");
  14. }
  15. static void ThreadLocal2()
  16. {
  17.     Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue.Value}");
  18. }
  19. static void ThreadLocal3()
  20. {
  21.     Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue.Value}");
  22. }
复制代码
执行结果如下:

而且可以通过ThreadLocal.Value 属性举行读取和写入,也就是通过_threadLocalValue.Value对变量举行赋值和取值。
03、volatile关键字

首先volatile关键字同样不是一个完整的线程同步机制,其主要作用是防止缓存和防止编译器优化。
在C#语言开辟中,由于编译器优化、JIT 编译、硬件缓存以及内存重排序等举动,很容易使得程序出现并发错误,尤其在多线程环境下这些情况会更为明显。虽然这些优化是在不影响程序逻辑的情况下举行的,但是由于重新排序对内存的读取和写入,进而可能导致数据竞争和同步问题。
volatile关键字就是为了告诉编译器和运行时:该字段的值可能会被多个线程同时修改,因此每次访问该字段时,都应该直接从主内存中读取,而不是使用寄存器或缓存中的值。这样可以防止 CPU 的优化举动导致某些线程读取到过期的值。
我们一起看看如下代码:
  1. //控制线程的标志
  2. private static bool _flag = false;
  3. //计数器
  4. private static int _counter = 0;
  5. public static void VolatileRun()
  6. {
  7.     var thread1 = new Thread(Volatile1);
  8.     var thread2 = new Thread(Volatile2);
  9.     thread1.Start();
  10.     thread2.Start();
  11.     thread1.Join();
  12.     thread2.Join();
  13.     //Console.WriteLine($"计数器最后的值: {counter}");
  14. }
  15. static void Volatile1()
  16. {
  17.     //注意:以下两行代码可能按相反的顺序执行
  18.     //设置计数器
  19.     _counter = 88;
  20.     //线程1:设置标志位,并且增加计数器
  21.     _flag = true;
  22. }
  23. static void Volatile2()
  24. {
  25.     //注意:_counter可能优先于_flag读取
  26.     //线程2:等待标志位变为 true,然后读取计数器
  27.     //等待 _flag 被设置为 true
  28.     while (!_flag) ;
  29.     //打印计数器值
  30.     Console.WriteLine($"当前计数器的值: {_counter}");
  31. }
复制代码
上面的代码很难在复现下面要说的问题,因此下面仅以此代码作为示例讲解。
上面代码的问题在于,经过编译器优化和内存重排序后, 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 编译、硬件缓存以及内存重排序等优化中排除,使用也很简单,可以如下使用:
  1. private static volatile bool _flag = false;
复制代码
另外volatile关键字不能引用于double,long,数组等类型,可以使用Volatile.Read和Volatile.Write静态方法来完成。
同时volatile关键字虽然可以解决许多并发问题,但是由于其不是原子操作,因此它并不能算是一个完整的线程同步机制,因此在多线程环境下照旧需要借助一些其他同步机制来保证线程安全。
因此volatile最大的应用场景就是在需要保证多个线程访问同一个共享变量时,大家都可以立刻看到最新的值,尤其是不涉及复杂操作如递增递减等。
:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

石小疯

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表