【C#】并行编程实战:同步原语(上)

news2024/10/7 8:21:39

        在第4章中讨论了并行编程的潜在问题,其中之一就是同步开销。当将工作分解为多个工作项并由任务处理时,就需要同步每个线程的结果。线程局部存储和分区局部存储,某种程度上可以解决同步问题。但是,当数据共享时,就需要用到同步原语。

        因篇幅所限,本章为上篇。本章主要介绍互锁操作、.NET中的内存屏障、锁原语。

        本教程对应学习工程:魔术师Dix / HandsOnParallelProgramming · GitCode


1、关于同步原语

        同步原语是基础平台(操作系统)提供的简单软件机制,它们有助于对内核进行多线程处理。同步原语在内部使用低级原子操作以及内存屏障(Memory Barrier),这意味着使用同步原语不必担心需要自己实现锁和内存屏障。

        同步原语的一些常见示例是锁(Lock)、互斥锁(Mutex)、条件变量(Conditional Variable)和信号量(Semaphore)。.NET Framework 提供了一系列同步原语,大致分为以下5类:

  • 互锁操作

  • 信号

  • 轻量级同步类型

  • Spin Wait

2、互锁操作

        互锁(Interlocked)的类封装了同步原语,并被用于为线程间共享的变量提供原子操作(Atomic Operation)。另外,Interlocked 类提供诸如 Increment、Decrement、Add、Exchange 和 CompareExchange 之类的方法。代码示例如下:

        private void RunAddValue()
        {
            TestValue = 0;
            var task = Task.Run(() =>
             {
                 var ret = Parallel.For(0, 1000, async x =>
                 {
                     await Task.Delay(x);
                     TestValue++;//理论上执行1000次,应该结果是1000;
                 });
             });
        }

        这里没有使用同步原语,TestValue 是我在属性面板上显示的值。那么点击运行后,等待一段时间,结果如下:

         这个值就不确定了,有时是998,有时是995或者其他的值,但总之都与期望值不匹配。这个原因就是线程竞争了,就是说两个线程同时在写入导致异常。要解决这个问题也很简单,代码修改如下:

TestValue++;//不考虑线程安全,结果可能不是1000
Interlocked.Increment(ref SafeTestValue);//线程安全,结果总是1000

        这里我们加了一个值来显示差异:

         在完成计数时,可以看到原子操作的值总是为 1000 , 而默认的方法不总是期望值。

        当然,Interlocked 类里还有很多别的操作,这里我认为就是按需要进行 API 调用即可,不需要再额外写代码示例了,大家可以参考以下资料学习:

Interlocked 类 (System.Threading) | Microsoft Learn为多个线程共享的变量提供原子操作。 icon-default.png?t=N658https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked?view=netstandard-2.1#methods

3、.NET 中的内存屏障

        在单核处理器上,只有一个线程获得 CPU 分片,而其他线程等待。这样当线程访问内存时,其顺序都是正确的,该模型称为顺序一致模型(Sequential Consistency Model)。

        多核处理器上,多个线程同时运行,系统中不能保证顺序一致,因为硬件或即时编译器(Just In Time,JIT)都可能会重新排序内存指令以提高性能。此外,处于提升缓存性能、负载推测(Load Speculation)或延迟存储操作等目的,也可能会对内存指令进行重新排序。

        出于性能考虑,当编译器遇到加载和存储语句时,它们并不总是以与编写时相同的顺序执行,而会对它们进行重新排序。

3.1、重新排序

        对于内存模型较弱的多核处理器(如 Intel Itanium 处理器),代码重新排序是以一个问题。但对于顺序一致模型,在单核情况下是没有影响的。

         对于同一段代码,其在不同运行环境下,其排序结果可能是不同的。

        这里为了说明,我们上一段示例代码:

        private static int TestValueA;
        private static int TestValueB;
        private static bool m_IsFinishOnce;

        public static void RunTestAddFunction()
        {
            TestValueA = 0;
            TestValueB = 0;

            Task.Run(() =>
            {
                Parallel.For(0, 10000, x =>
                {
                    TestValueA = x;
                    TestValueB = x;
                    m_IsFinishOnce = TestValueA >= TestValueB;
                });
            });
        }

        public static void DebugResult()
        {
            Task.Run(() =>
            {
                Parallel.For(0, 10000, x =>
                {
                    if (!m_IsFinishOnce)
                    {
                        Debug.LogError($"值不对了:{TestValueA} >= {TestValueB} = {m_IsFinishOnce}");
                    }
                });
                Debug.Log("测试完成");
            });
        }

        按照道理来讲,m_IsFinishOnce 应该一直为true才对,毕竟我们这段代码,A、B都是同时赋值的。当我们多线程运行的时候,却发现情况并非如此:

         多次测试中,偶尔会发生一两次 A B 的值并不相等的情况。而 m_IsFinishOnce 的值,前一行还为 false,后一行就为 true 了(这其实在多线程编程中很常见)。我觉得可能的解释就是这段代码进行重新排序了,并不是严格按照 赋值A → 赋值B → 判定相等的顺序执行的。

        当然,上述代码也有另一种情况:

         多线程操作中,虽然每次代码排序是正确的,但是由于多个线程在同时写入,导致读取时拿不到正确的值(如红框中所示)。总之上述的代码就是错误的多线程操作代码,这里只是为了演示。

3.2、内存屏障的类型

        内存屏障的意义在于确保屏障之上和之下的任何代码语句都不会越过屏障,从而强制保证代码顺序。内存屏障有以下 3 种类型:

  • 存储(写入)内存屏障:不让存储操作跨屏障移动

  • 加载(读取)内存屏障:不让加载操作跨屏障移动

  • 全能型内存屏障(Full Memory Barrier):不让存储和加载操作跨屏障移动

        C# 的 Interlocked.MemoryBarrier() 就是一种全能型内存屏障:

Interlocked.MemoryBarrier 方法 (System.Threading) | Microsoft Learn按如下方式同步内存存取:执行当前线程的处理器在对指令重新排序时,不能采用先执行 MemoryBarrier() 调用之后的内存存取,再执行 MemoryBarrier() 调用之前的内存存取的方式。 icon-default.png?t=N658https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked.memorybarrier?source=recommendations&view=netstandard-2.1        而 Interlocked.MemoryBarrierProcessWide 则是一种进程范围和系统范围的内存屏障。

Interlocked.MemoryBarrierProcessWide 方法 (System.Threading) | Microsoft Learn提供覆盖整个过程的内存屏障,确保来自任何 CPU 的读写都不能越过该屏障。 icon-default.png?t=N658https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked.memorybarrierprocesswide?view=netstandard-2.1

3.3、避免使用构造对代码进行重新排序

        书上这一章节,说实话没有看懂。书上说要使用内存屏障避免操作越过屏障,但是我使用时并没有感觉到有什么明显变化…… 3.1的示例代码我尝试了很多方式,并不能实现保证执行顺序。

        只是书上提到,尽量不要用 Thread.MemoryBarrier ,而用 Interlocked.MemoryBarrier 代替。

        我想的是,可能在一些追求性能的无锁代码,会使用内存屏障,而在关键位置还是要用锁。当然也可能是我这里没有正确使用内存屏障。如果后面研究结果下来,内存屏障的正确用法,我会补充在这里。如果内存屏障不是重要知识点,就忽略这一章。

4、锁原语

        锁可用于限制对受保护资源的访问,使受保护的资源尽可以被单个现场或一组线程访问。当锁应用于共享资源时,需要执行以下步骤:

  • 一个线程或一组线程通过获取锁来访问共享资源。

  • 其他无法访问锁的线程进入等待状态。

  • 一旦有线程释放了锁,另一个线程就会获取该锁,并开始执行。

4.1、线程状态

        在线程的生命周期的任何时候,都可以使用该线程的 ThreadState 属性来查询线程状态:

ThreadState 枚举 (System.Threading) | Microsoft Learn指定 Thread 的执行状态。 icon-default.png?t=N658https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.threadstate?view=netstandard-2.1#--        简单介绍如下:

namespace System.Threading
{
    [Flags]
    public enum ThreadState
    {
        Running = 0,//运行中
        StopRequested = 1,//等待停止
        SuspendRequested = 2,//已调用 Suspend 方法被请求挂起
        Background = 4,//后台线程
        Unstarted = 8,//未启动
        Stopped = 16,//已停止
        WaitSleepJoin = 32,//通过调用 Wait、Sleep、Join方法,导致该线程阻塞
        Suspended = 64,//已挂起
        AbortRequested = 128,//调用 Abort 方法,但是尚未终止,而是等待 ThreadAbortException 终止线程
        Aborted = 256//已终止
    }
}

        各个状态的切换关系如下:

4.2、阻塞与自旋

        阻塞的线程在指定时间内放弃了处理器的时间片,这样,处理器的时间片就可以用于其他线程以提高性能。但是,这也增加了上下文切换的开销。因此,在线程会阻塞相当长的时间的时间才意义。

        如果等待时间很短,则在不放弃处理器时间片的情况下进行自旋是很有意义的。例如写个死循环用于检查工作进度,虽然浪费了处理器时间,但如果等待时间不是很长,仍然可以显著提高性能。


(未完待续)

 本教程对应学习工程:魔术师Dix / HandsOnParallelProgramming · GitCode

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python实现PSO粒子群优化算法优化XGBoost回归模型(XGBRegressor算法)项目实战

说明:这是一个机器学习实战项目(附带数据代码文档视频讲解),如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 PSO是粒子群优化算法(Particle Swarm Optimization)的英文缩写,是一…

在线培训系统成为未来自主学习利器

传统教育模式存在的一个重要问题是,学生往往需要跟随教师和课堂的步调前进。这种模式可能会导致一些学生在学习过程中感到压力和挫败,并且不可避免地会出现部分学生落后于其他同学的情况。然而,在线培训系统可以赋予学生更多的自主权&#xf…

Linux系统编程(环境变量编程)

文章目录 前言一、环境变量表二、环境变量读写接口总结 前言 本篇文章我们来讲解环境变量编程,环境变量在Linux中可以说是非常重要的,那么这篇文章将会带大家来学习环境变量的编程。 一、环境变量表 在Linux系统中,环境变量是一种特殊的变…

SQL-每日一题【595.大的国家】

题目 World 表: 如果一个国家满足下述两个条件之一,则认为该国是 大国 : 面积至少为 300 万平方公里(即,3000000 km2),或者人口至少为 2500 万(即 25000000) 编写一个…

kafka第一课-Kafka快速实战以及基本原理详解

一、Kafka介绍 Kafka是一个分布式的发布-订阅消息系统,可以快速地处理高吞吐量的数据流,并将数据实时地分发到多个消费者中。Kafka消息系统由多个broker(服务器)组成,这些broker可以在多个数据中心之间分布式部署&…

react学习笔记——复习模块

前言:最近开始学习react,之前学习vue没有把笔记整理的特别好,非常后悔,感觉学了等于没学,这次要好好整理啊!本次学习参考教程为B站,张天禹老师的react全家桶。 文章目录 类的基本知识创建一个类…

时间序列分类 论文和数据集汇总

时间序列分类 时间序列广泛应用于金融、工业领域、健康、运维、交通领域。 其实异常检测任务也可以看作是一个时间序列分类任务,异常与否两类,或者异常有很多种,则是多分类问题。 时间序列分类的数据是多种的:时间轨迹数据&#x…

Real-time Short Video Recommendation on Mobile Devices 阅读笔记

摘要 用户会实时兴趣转移,为实现在客户端重排,提出一种 context-aware re-ranking 方法,基于 adaptive beam search 1 引言 1.1 之前架构的问题: 1,需解决 real-time feedback 的问题 2,速度问题 1.2…

【Spring Boot】开发环境热部署

开发环境热部署 在实际的项目开发调试过程中会频繁地修改后台类文件,导致需要重新编译、重新启动,整个过程非常麻烦,影响开发效率。下面介绍Spring Boot如何解决这个问题。 1.devtools实现原理 我们在开发调试Spring Boot项目时&#xff0…

Flink 系列三 Flink 实战

目录 ​编辑 前言 1、安装flink环境 2、在idea中创建flink的第一个demo 2.1、执行如下maven命令 2.2、填写groupId、artifactId、version、package 2.3、选择Yes即可生成创建好的工程 3、开发第一个flink程序 3.1、开发一个简单的统计程序 3.2、直接编译得到jar包 4、…

【Java基础】AQS (AbstractQueuedSynchronizer) 抽象队列同步器

关于作者:CSDN内容合伙人、技术专家, 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 ,擅长java后端、移动开发、人工智能等,希望大家多多支持。 目录 一、导读1.1 CLH锁 二、概览三、使用场景3.1 AQS 对资源的共享方式…

【自我提升】JPA从搭建到CRUD快速入门(IDEA+MAVEN)

写在前面:今天又学习一点新的东西,方面日后查询和巩固学习,下面将学习过程记录下来。 一、创建MAVEN工程 1. 打开IDEA创建一个纯净的maven工程项目 2. 打开pom文件,导入maven坐标 注意:我使用的postgres数据库&#x…

【微服务】搭建项目以及子模块

目录 方法一:搭建远程仓库新建idea 项目创建子模块创建父模块的pom父模块的gitignore文件修改查看远程代码仓库 方法二 方法一: 搭建远程仓库 选择 下载地址 新建idea 项目 复制 项目远程仓库的下载地址 下载完成 创建子模块 导入微服务的必要组件…

Apache Pulsar 分布式部署

1.Pulsar 简介 Pulsar 是一个支持多租户的、高性能的消息中间件;最初是由 Yahoo 研发的开源,分布式pub-sub系统,现在是Apache的一个顶级开源项目 Pulsar 提供了四种订阅类型,它们可以共存在同一个主题上,以订阅名进行区…

【html页面引入vue3语法模板】在html页面中使用vue3语法和elementul-plus组件库的简单模板

前言 这是最近在看这些东西,因为看别的地方是用脚手架直接用的。 我这个项目想要在html上直接使用。 所以我就试了下如何在html上使用vue3语法 目前摸索出来的是这样可以使用。 记录下来供参考,如果有不好的地方后续改进 效果图 这里就是简单的试了一…

修改npm路径

npm config ls如果是第一次使用NPM安装包的话,在配置中只会看到prefix的选项,就是NPM默认的全局安装目录。但是如果有多次使用NPM安装包的话,就会看到cache和prefix两个路径。 新建两个文件夹node_global_modules和node_cache npm config s…

mac android studio设置跟mac系统一样的快捷键

mac版的android studio 跟mac系统的快捷键不一样,主要修改了下面几组操作,为了跟mac系统快捷键相同 setting->Keymap 搜索bottom 修改3个快捷键: cmd↓ 设置让鼠标移动到屏幕最后面 shiftcmd↓ 选中从当前位置到屏幕最下面 option↓. 或者 end 滚动到屏幕最下方 // 因为默认…

详解 HTTPS、TLS、SSL、HTTP区别和关系

一、什么是HTTPS、TLS、SSL HTTPS,也称作HTTP over TLS。TLS的前身是SSL,TLS 1.0通常被标示为SSL 3.1,TLS 1.1为SSL 3.2,TLS 1.2为SSL 3.3。下图描述了在TCP/IP协议栈中TLS(各子协议)和HTTP的关系。 二、HTTP和HTTPS协议的区别 …

【开源项目】中后台开发框架vue-next-admin

vue-next-admin 基本介绍 基于 vue3.x CompositionAPI setup 语法糖 typescript vite element plus vue-router-next pinia 技术,适配手机、平板、pc 的后台开源免费模板,希望减少工作量,帮助大家实现快速开发。 在线预览 账号: adm…

Braindecode系列 (1):在BCIC IV 2a数据集上进行试验

Braindecode系列:在BCIC IV 2a数据集上进行试验 0. 引言1. 环境介绍1.1 环境配置1.2 运行环境 2. Python实现2.1 加载和预处理数据集2.2 创建模型2.3 模型训练2.4 结果输出图像 3. 结果展示4. 总结 0. 引言 最近在看运动想象相关的论文时,找到了一个很好…