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

news2024/12/24 8:17:06

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

        因篇幅所限,本章为第1篇。本章主要介绍互锁操作、.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为多个线程共享的变量提供原子操作。https://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() 调用之前的内存存取的方式。https://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 的读写都不能越过该屏障。https://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 的执行状态。https://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/745897.html

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

相关文章

Chrome内建DNS导致的解析错误修复

Index Chrome内建DNSDisable Async DNS resolver Chrome内建DNS 实际上 , Chrome在使用自己的DNS来进行域名的解析 , 这导致有时候一些域名解析会出现错误 , 导致访问速度变慢 , 例如 blog.csdn.net 使用谷歌的 8.8.8.8 dns解析就会 , 解析到香港的ip上去 , 导致访问速度变慢 …

群晖NAS:docker查询注册表失败解决方案 docker安装网心云、mysql等

群晖NAS:docker查询注册表失败解决方案 差不多2023年4月底开始的,docker内不能直接搜索注册表。据说是有人在库里放了一些有意思的东西,被和谐掉了,所以也别指望什么时候能解封。 网上很多案例,都不能用。还有奇葩的…

史上最细接口测试详解,接口测试从0到1实施,一篇打通...

目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 1、接口测试描述定…

【C语言】-- 死循环了怎么办?

#include <stdio.h> int main() {int i 0;int arr[] {1,2,3,4,5,6,7,8,9,10};for(i0; i<12; i){arr[i] 0;printf("hello\n");}return 0; } 阅读上面这个代码&#xff0c;我们会认为这不就是简单的数组访问越界么。那么这段代码就应该会报错&#xff0c;…

macOS Sonoma 14beta 3 (23A5286i)第二个更新「附黑/白苹果镜像下载」

系统镜像下载&#xff1a; 系统介绍 黑果魏叔 7 月12 日消息&#xff0c;苹果今天发布 macOS Sonoma 14.0 Beta 3&#xff08;内部版本号&#xff1a;23A5286i&#xff09;第二个更新。 目前尚不清楚苹果为什么要发布 macOS Sonoma Beta 3 的第二个版本&#xff0c;但它可能…

外包干了2年,我裸辞了...

我25岁&#xff0c;中级测试&#xff0c;外包&#xff0c;薪资13.5k&#xff0c;人在上海。内卷什么的就不说了&#xff0c;而且人在外包那些高级精英年薪大几十的咱也接触不到&#xff0c;就说说外包吧。 假设以我为界限&#xff0c;25岁一线城市13.5k&#xff0c;那22-24大部…

CUDA11.1、cuDNN8.6.0、Tensorrt8.5.3,ubuntu20.04安装过程记录

CUD11.1 下载地址&#xff1a;CUDA Toolkit Archive | NVIDIA Developer 安装&#xff1a; wget https://developer.download.nvidia.com/compute/cuda/11.1.1/local_installers/cuda_11.1.1_455.32.00_linux.run sudo sh cuda_11.1.1_455.32.00_linux.run 对于不是sudo用户&…

CRYPTO-36D-飞鸽传书

0x00 前言 CTF 加解密合集&#xff1a;CTF 加解密合集 0x01 题目 TVdJd09HRm1NamMyWkdKak56VTVNekkzTVdZMFpXVTJNVFl5T0Rrek1qUWxNRUZsTW1GbE0yRXlNelV3TnpRell6VXhObU5rWVRReE1qUTVPV0poTTJKbE9TVXdRV0prWlRVeVkySXpNV1JsTXpObE5EWXlORFZsTURWbVltUmlaRFptWWpJMEpUQkJaVEl6…

Jmeter性能测试插件jpgc的安装

目录 一、获取插件包 1.访问官网获取 2.百度网盘下载 二、安装路径 三、安装插件 1.重启Jmeter 2.进入Plugins Manager 3.jpgc插件安装 4.安装完成后检查 总结&#xff1a; 一、获取插件包 1.访问官网获取 官网地址&#xff1a; ​ 2.百度网盘下载 链接&#xff1…

LiveGBS 国标平台作为下级GB28181级联到海康大华宇视华为等第三方国标平台的操作步骤说明

LiveGBS 国标平台作为下级GB28181级联到海康大华宇视华为等第三方国标平台的操作步骤说明 1、什么是GB/T28181级联2、搭建GB28181国标流媒体平台3、获取上级平台接入信息3.1、如何提供信息给上级3.2、上级国标平台如何添加下级域3.2、接入LiveGBS示例 4、配置国标级联4.1、国标…

Go语言对json处理总结

实际业务开发中&#xff0c;json处理很常见&#xff0c;本文总结一下go语言对json的处理。 目录 1.encoding/json 包 1.1 Marshal 函数 &#xff08;1&#xff09;原始字段名 &#xff08;2&#xff09;字段重命名 1.2 Unmarshal函数 &#xff08;1&#xff09;原始字段…

ELK-日志服务【logstash-安装与使用】

目录 【1】安装logstash logstash input 插件的作用与使用方式 【2】input --> stdin插件&#xff1a;从标准输入读取数据&#xff0c;从标准输出中输出内容 【3】input -- > file插件&#xff1a;从文件中读取数据 【4】input -- > beat插件&#xff1a;从filebe…

目标检测学习

目录 1、目标定位 2、特征点检测 3、目标检测 4、滑动窗口的卷积实现 5、Bounding Box 预测&#xff08;Bounding box predictions&#xff09; 6、交并化 7、非极大值抑制 8、Anchor Boxes 9、YOLO算法 1、目标定位 2、特征点检测 如何检测特征点&#xff08;以人的部…

基于linux下的高并发服务器开发(第一章)- 静态库的制作1.4

01 / 什么是库 库文件是计算机上的一类文件&#xff0c;可以简单的把库文件看成一种代码仓库&#xff0c;它提供给使用者一些可以直接拿来用的变量、函数或类库是特殊的一种程序&#xff0c;编写库的程序和编写一般的程序区别不大&#xff0c;只是库不能单独运行。库文件有两种…

如何选择适合外贸公司的企业邮箱?推荐哪些优质企业邮箱服务?

为外贸公司选择合适的企业邮箱是企业成功经营的关键。强大、安全、直观的企业邮箱能够满足不同平台上不同用户的需求&#xff0c;这是确保数据和消息与客户和合作伙伴准确沟通的关键。以下是外贸公司在选择企业邮箱时应考虑的一些规范: 1、安全 在考虑企业邮箱时&#xff0c;安…

如何下载centOS镜像

我们在操作虚拟机的时候都有一个选择镜像&#xff0c; 这里我们可以去对应的官网去下载即可&#xff0c;下面就是网址 Download (centos.org) 就会出现许多地址 我们只需要随便选一个地址即可&#xff08;前提它能用&#xff09;&#xff0c; 到了下图即可点击下载&#xff0c;…

通信算法之179: 单载波频域均衡系统的帧结构2

一。帧结构 &#xff08;2&#xff09; &#xff08;3&#xff09;

问懵了....美团一面索命44问,过了就60W+

说在前面 在40岁老架构师尼恩的&#xff08;50&#xff09;读者社区中&#xff0c;经常有小伙伴&#xff0c;需要面试美团、京东、阿里、 百度、头条等大厂。 下面是一个小伙伴成功拿到通过了美团一次技术面试&#xff0c;最终&#xff0c;小伙伴通过后几面技术拷问、灵魂拷问…

欧姆龙CJ系列PLC以太网通讯处理器欧姆龙cp1h以太网模块

捷米特JM-ETH-CJ转以太网模块是一款经济型的以太网通讯处理器&#xff0c;是为满足日益增多的工厂设备信息化需求&#xff08;设备网络监控和生产管理&#xff09;而设计&#xff0c;用于欧姆龙CJ1/CJ2/CS1系列PLC的以太网数据采集&#xff0c;非常方便构建生产管理系统。 捷米…

Mybatis-plus生成代码

生成类 package com.lbdj.user.service;import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.config.rules.DateType; import com.lbdj.user.service.co…