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

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

        因篇幅所限,本章为第4篇,主要介绍轻量级同步原语、屏障和倒数事件、SpinWait和自旋锁。

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


7、轻量级同步原语

        .NET Framework 还提供了轻量级的同步原语,其性能比同步原语更好。它们尽可能避免依赖内核对象(如等待句柄),因此它们只在进程内部工作。适合在线程等待时间很短的时候使用。

        从微软的介绍来看,在单进程工作的情况下,轻量级同步原语性能会更好,使用上也是相同的。建议作为优先选择。

7.1、ReaderWriterLockSlim

        相当于 ReaderWriterLock 的轻量级实现。其允许多个线程访问受保护资源,而仅允许一个线程写入。

ReaderWriterLockSlim 类 (System.Threading) | Microsoft Learn表示用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问。 icon-default.png?t=N6B9https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.readerwriterlockslim?view=netstandard-2.1

        虽然 ReaderWriterLockSlim 类似于 ReaderWriterLock,但不同之处在于,前者简化了递归规则以及锁状态的升级和降级规则。 ReaderWriterLockSlim 避免了许多潜在的死锁情况。

        另外,ReaderWriterLockSlim 的性能显著优于 ReaderWriterLock。 建议对所有新开发的项目使用 ReaderWriterLockSlim。

7.2、SemaphoreSlim

        轻量级信号灯 SemaphoreSlim 是 Semaphore 的轻量级实现,限制了多线程的访问。SemaphoreSlim 只能是局部信号灯,而 Semaphore 可以创建为全局信号灯

SemaphoreSlim 类 (System.Threading) | Microsoft Learn对可同时访问资源或资源池的线程数加以限制的 Semaphore 的轻量替代。 icon-default.png?t=N6B9https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.semaphoreslim?view=netstandard-2.1

        使用区别就是用 Wait 方法替代了 WaitOne 方法。

7.3、ManualResetEventSlim

        ManualResetEventSlim 是 ManualResetEvent 的轻量级实现,具有更好的性能。使用时也是,用 Wait 方法替代了 WaitOne 方法。

ManualResetEventSlim 类 (System.Threading) | Microsoft Learn表示线程同步事件,收到信号时,必须手动重置该事件。 此类是 ManualResetEvent 的轻量替代项。 icon-default.png?t=N6B9https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.manualreseteventslim?view=netstandard-2.1        从介绍来看,在单进程中,使用 ManualResetEventSlim 大部分情况下都有更好的优势。

8、屏障和倒数事件

        .NET Framework 有一些内置的信号原语,可以帮助我们同步多个线程。

8.1、CountDownEvent

        在计数为 0 时发出信号的倒数事件:

CountdownEvent 类 (System.Threading) | Microsoft Learn表示在计数变为零时处于有信号状态的同步基元。 icon-default.png?t=N6B9https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.countdownevent?view=netstandard-2.1        简单示例代码如下:

        public static void RunWithCountDownEvent()
        {
            CountdownEvent countdownEvent = new CountdownEvent(5);//等待5个任务完成

            for (int i = 0; i < 10; i++)//同时执行10个任务
                RandomTimtToCountDown(countdownEvent, i);

            Task.Run(() =>
            {
                countdownEvent.Wait();
                Debug.LogError("倒数完成 !");
            });
        }
        
        public static void RandomTimtToCountDown(CountdownEvent countdownEvent, int index)
        {
            Task.Run(async () =>
            {
                System.Random random = new System.Random();
                int waitTime = random.Next(500, 2000);
                //Debug.Log($"开始等待 {waitTime} ms !");
                await Task.Delay(waitTime);
                Debug.Log($"【{index}】等待 {waitTime} ms 完成");
                countdownEvent.Signal();//发出信号
            });
        }

        运行结果如下:

        可见,倒数事件能保证5个事件完成时解除阻塞。当我们需要等待多个任务完成一定进度,而不需要在意究竟是哪些任务完成时,就可以使用这个事件。这个我感觉在某些特殊情况很有用,例如我需要同时加载 100 个资源,但是只要加载了其中任意 30 个就可以继续游戏了,就可以使用这个倒数事件。

8.2、Barrier

        允许多个线程运行,而无需主线程控制他们,创建了一个屏障直至所有线程到达。

Barrier 类 (System.Threading) | Microsoft Learn使多个任务能够采用并行方式依据某种算法在多个阶段中协同工作。 icon-default.png?t=N6B9https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.barrier?view=netstandard-2.1        测试代码如下:

        public static void RunWithBarrier()
        {
            Barrier barrier = new Barrier(0);//设定初始参与者为 0 

            RandomTimeWaitBarrier(barrier);
            RandomTimeWaitBarrier(barrier);
            RandomTimeWaitBarrier(barrier);
        }        
        
        public static void RandomTimeWaitBarrier(Barrier barrier)
        {
            barrier.AddParticipant();//添加一个参与者
            Task.Run(async () =>
            {
                System.Random random = new System.Random();
                int waitTime = random.Next(500, 2000);
                await Task.Delay(waitTime);
                Debug.Log($"等待 {waitTime} ms 完成");
                barrier.SignalAndWait();//标记完成,并等待其他任务完成
                Debug.LogError("执行完毕!");
            });
        }

        运行结果如下:

         屏障的作用很明显,就是根据参与者的完成情况,然后同时进行剩余步骤。这个的作用就很多了,例如经常遇到的需求,就是一边加载资源一边等待网络消息,但是要两者都完成才能进行下一步。因为我们不知道哪个任务先完成,一般做法是设置很多 Flag ,互相监听等待情况。使用屏障,就能很方便地能实现这个功能。

9、SpinWait

        在 4.2、阻塞与自旋 中提到过,如果阻塞的时间很短,采用自旋技术比阻塞有效得多。因为自旋减少了上下文切换和转换的开销(也就是上下文切换的开销大于阻塞的开销,使用自旋)。

        SpinWait 的用法也很简单:

SpinWait spin = new SpinWait();
spin.SpinOnce();

        这样就能完成一次极快的阻塞。

        也可以使用 SpinWait.SpinUntil ,传入一个方法,当返回会 true 的时候结束阻塞。

SpinWait 结构 (System.Threading) | Microsoft Learn为基于自旋的等待提供支持。 icon-default.png?t=N6B9https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.spinwait?view=netstandard-2.1        SpinWait 成员不是线程安全的。 如果多个线程必须旋转,则每个线程都应使用其自己的实例 SpinWait。

10、自旋锁

        如果等待时间极短,则锁和互锁原语可能会大大降低性能。自旋锁 (SpinLock)提供了一种轻量级的低级替代选项。SpinLock 即使还没有获得锁,也会产生线程的时间片。

        这里我们又搬出 3.1、重新排序 那一段示例代码,这次我们加上自旋锁:

        public static void RunTestAddFunctionWithSpinLock()
        {
            TestValueA = 0;
            TestValueB = 0;
            m_IsFinishOnce = false;

            Task.Run(() =>
            {
                SpinLock spinLock = new SpinLock();

                Parallel.For(0, 10000, x =>
                {
                    bool lockToken = false;
                    spinLock.Enter(ref lockToken);
                    TestValueA = x;
                    TestValueB = x;
                    m_IsFinishOnce = TestValueA >= TestValueB;
                    spinLock.Exit(false);
                });
                Debug.Log("运行完成");
            });
        }

        这次也达到了其他上锁代码同样的效果,没有再出现取值错误的问题。这里恰好满足自旋锁的使用条件:任务量很小,用自旋锁来避免上下文的切换

SpinLock | Microsoft Learn详细了解:SpinLockicon-default.png?t=N6B9https://learn.microsoft.com/zh-cn/dotnet/standard/threading/spinlock


本章小节:

        本章详细讲解了 .NET Core 提供的同步原语,如果并行代码要保证正确,使用同步原语非常重要。当然,这会带来额外的性能开销,所以最好只在关键节使用。

        另外,尽量使用轻量级同步原语;而且屏障、倒数和自旋也是非常有用的。

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

相关链接:

【C#】并行编程实战:同步原语(1)_魔术师Dix的博客-CSDN博客

【C#】并行编程实战:同步原语(2)_魔术师Dix的博客-CSDN博客

【C#】并行编程实战:同步原语(3)_魔术师Dix的博客-CSDN博客

猜你喜欢

转载自blog.csdn.net/cyf649669121/article/details/131716840