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

news/2025/1/29 22:35:29/文章来源:https://www.cnblogs.com/hugogoos/p/18694925

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

01、ThreadStatic

严格意义上来说这两个并不是实现线程同步方案,而是解决多线程资源安全问题,而我们研究线程同步最终也是为了解决多线程资源安全问题,因此就先说下这两个用法。

ThreadStatic特性可以实现线程本地存储,使得每个线程都有一个独立的字段副本。从而避免不同线程间共享资源。

使用ThreadStatic时需要注意以下几点:

1、ThreadStatic仅能作用于静态字段;。

2、ThreadStatic字段不应使用内联初始化。

3、每个线程都会有独立的_threadLocalVariable实例,当线程退出时,相关的线程本地存储会被清除。

4、由于 ThreadStatic 是线程局部存储,它并不是跨线程共享数据的解决方案。

使用起来也很简单,我们来着重说说上面注意点的第二点,虽然语法上可以写出内联初始化,但是这样会导致一个问题:仅有访问其的首个线程上可以获取其初始化变量值,而其他所有线程都只能获取到变量类型的默认值。比如下面这段代码:

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

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 的优化行为导致某些线程读取到过时的值。

我们一起看看如下代码:

//控制线程的标志
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 被设置为 truewhile (!_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 编译、硬件缓存以及内存重排序等优化中排除,使用也很简单,可以如下使用:

private static volatile bool _flag = false;

另外volatile关键字不能引用于double,long,数组等类型,可以使用Volatile.Read和Volatile.Write静态方法来完成。

同时volatile关键字虽然可以解决许多并发问题,但是因为其不是原子操作,因此它并不能算是一个完整的线程同步机制,因此在多线程环境下还是需要借助一些其他同步机制来保证线程安全。

因此volatile最大的应用场景就是在需要保证多个线程访问同一个共享变量时,大家都可以立刻看到最新的值,尤其是不涉及复杂操作如递增递减等。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/876941.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习基础理论————混合专家模型(MoE)/KV-cache

1、混合专家模型(MoE) 参考HuggingFace中介绍:混合专家模型主要由两部分构成: 1、稀疏的MoE层:这些层代替了传统 Transformer 模型中的前馈网络 (FFN) 层。MoE 层包含若干“专家”(例如 8 个),每个专家本身是一个独立的神经网络。在实际应用中,这些专家通常是前馈网络 (…

gin: 使用独立的路由文件和controller文件

一,目录结构:二,代码: 1,controller/ImageController.go package controllerimport ("github.com/gin-gonic/gin""net/http" )type ImageController struct{}func NewImageController() ImageController {return ImageController{} }//得到详情 func (i…

2024年终总结——我自风中来,又往风中去

目录前言碎语事件简单记录(参考自己的朋友圈)疑为前城去连云港——小青岛,南北交,桃花源记忆点不大的一些城市……上海——夜之城北京——梦之城似是故人来技术或科研——向现实进发生活——认识我,改变我绩点——继续维稳比赛——淡化、反思音乐——重拾展望——勇敢的向…

Java 序列化流

目录概述ObjectOutputStream类构造方法序列化操作ObjectInputStream类构造方法反序列化操作1反序列化操作2 概述 Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据、对象的类型和对象中存储的属性等信息。字节序列写出到文件之后…

【持续更新】【专题】初等数论【更新逆元】

【持续更新】【专题】初等数论 Designed By:FrankWkd 【100%原创】【禁止搬运】 Updated at 2025.01.26 前言:主要从线性筛开始速通初等数论 尽可能的多证明结论而不是阐述结论。如果你只是想回顾结论,请看其他人的 \(Blog\) .一、基础概念整除:对于两个正整数 \(a,b\), 存…

四.3 Redis 五大数据类型/结构的详细说明/详细使用( hash 哈希表数据类型详解和使用)

四.3 Redis 五大数据类型/结构的详细说明/详细使用( hash 哈希表数据类型详解和使用) @目录四.3 Redis 五大数据类型/结构的详细说明/详细使用( hash 哈希表数据类型详解和使用)2.hash 哈希表常用指令(详细讲解说明)2.1 hset <key><field><value> 给<…

qcom usb PD tcpc overview

该软件层将PMxxxxB硬件连接到LPM模块,因为上述模块使用Type-C端口控制器接口(TCPCI)进行通信。 软件层,使PMIC Type-C PD硬件适用于基于TCPCI的软件架构 基本状态机:进入、离线、待机状态 PMIC Type-C和PD PHY中断的消费者 PMIC硬件专用排序和定时器

男生如何自己简单理发

快过年了,给自己理个发。 从24年10月份开始,目前已经给自己理发两次,都是短发寸头,给我爸和我三叔各理发一次,算是有点经验了,我准备过年前给自己再稍微修理头发一下。自己动手实践,且效果还不错的情况下,真的非常有成就感,如果有人指导情况下,其实自己理发难度不高,…

测序中的GC偏好

001、 测序中的GC偏好指的是基因组上GC含量在50%左右的区域更容易被测到,产生的reads更多,这些区域的覆盖度更高,在高GC或者低GC区域,不容易被测到,产生较少的reads,这些区域的覆盖度更少。用基因组单位长度的bin中的GC含量作为横坐标,覆盖度作为纵坐标作图,可以明显的…

人工智能(AI)简史:推动新时代的科技力量

人工智能(AI,Artificial Intelligence)是计算机科学的一个分支,旨在研究和开发可以模拟、扩展或增强人类智能的**系统**。它涉及多种技术和方法,包括机器学习、深度学习、自然语言处理(NLP)、计算机视觉、专家系统等。一、人工智能简介 人工智能(AI,Artificial Intell…

03. vim编辑器的使用

一、vim编辑器的使用vim 是 Unix 和 类 Unix 操作系统中常用的文本编辑器。如果 Ubuntu 系统默认没有安装 vim,我们可以使用 apt 工具安装 vim 编辑器。sudo apt install vim安装好 vim 之后,我们可以如下命令编辑一个文件。 vim 文件用 vim 打开一个文件就直接进入了 一般模…

“简单”学英语

本文总结了本人作为英语学渣的较为无痛的学习英语方法,无痛当然学习时间长一些,但相对简单一些,实操性强。 前言 最近和人聊天时对方说,感觉没什么可以学,但又想学点什么,我回答:”学英语啊“。有人迷茫,不知道怎么发展时,我回答:“先学点英语啊”。有人在犹豫要不要…