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

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

        因篇幅所限,本章为第3篇。本章主要介绍信号原语。

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


6、信号原语

        并行编程的一个重要方面是任务协调。

        在创建任务时,可能会遇到 生产者/消费者(Producer/Consumer)场景,其中一个线程(消费者)在等待另一个线程(生产者)更新共享资源。由于消费者不知道生产之何时更新共享资源,因此它将轮询共享资源,可能导致竞争,且轮询的效率很低。最好使用 .NET Framework 提供的信号原语(Singaling Primitive):消费者线程将暂停,直到从生产者线程接收到信号为止。

6.1、Thread.Join

Thread.Join 方法 (System.Threading) | Microsoft Learn在此实例表示的线程终止前,阻止调用线程。https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.thread.join?view=netstandard-2.1#system-threading-thread-join        Thread.Join 是使线程等待另一个线程信号的最简单方法,示例代码如下:

        private void RunWithThreadJoin()
        {
            int result = 0;
            Thread th1 = new Thread(() =>
            {
                Debug.Log("Th1 Start !");
                Thread.Sleep(1000);
                result = 100;
                Debug.Log("Th1 End !");
            });

            Thread th2 = new Thread(() =>
            {
                Debug.Log($"Th2 Result Start: {result}");
                th1.Join();
                Debug.Log($"Th2 Result End: {result}");
            });

            th1.Start();
            th2.Start();
        }

        此代码运行结果如下:

【C#】并行编程实战:同步原语(3)_第1张图片

6.2、AutoResetEvent

        AutoResetEvent 是指自动重置的 WaitHandle 类。重置后,允许一个线程通过创建的屏障,一旦线程通过,它们就会再次被设置,从而阻塞线程直到下一个信号。

AutoResetEvent 类 (System.Threading) | Microsoft Learn表示线程同步事件在一个等待线程释放后收到信号时自动重置。 此类不能被继承。https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.autoresetevent?view=netstandard-2.1        首先我们写一段没有线程安全的代码:

        private void RunWithAutoResetEvent()
        {
            int result = 0;
            Parallel.For(0, 1001, x =>
            {
                result += x;
            });
            Debug.Log($"Result = {result}");
        }

        显然这段代码是的结果,理论上是 500500,但实际上多运行几次,结果总会有所不同:

【C#】并行编程实战:同步原语(3)_第2张图片

         这个原因就不赘述了,很显然了。下面用 AutoResetEvent 来进行改造:

        private void RunWithAutoResetEvent()
        {
            AutoResetEvent autoResetEvent = new AutoResetEvent(false);

            Task.Run(() =>
            {
                int result = 0;
                autoResetEvent.Set();
                Parallel.For(0, 1001, x =>
                {
                    autoResetEvent.WaitOne(100);//会阻塞线程!
                    result += x;
                    autoResetEvent.Set();
                });
                Debug.Log($"Result = {result}");
            });
        }

        最后结果达到预期,没有了线程竞争的问题:

 (17次执行结果都是500500)

6.3、ManualResetEvent

ManualResetEvent 类 (System.Threading) | Microsoft Learn表示线程同步事件,收到信号时,必须手动重置该事件。 此类不能被继承。https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.manualresetevent?source=recommendations&view=netstandard-2.1        ManualResetEvent 是指需要手动重置的等待句柄,允许多个线程通过,直到它再次被设置。


        private ManualResetEvent manualResetEvent = new ManualResetEvent(false);
        private bool IsReset;

        private void RunWithManualResetEvent()
        {
            Task.Run(async () =>
            {
                for (int i = 0; i < 10; i++)
                {
                    manualResetEvent.WaitOne();
                    await Task.Delay(1000);
                    Debug.Log("Task 1  Loop :  " + i);
                }
            });

            Task.Run(async () =>
            {
                for (int i = 0; i < 10; i++)
                {
                    manualResetEvent.WaitOne();
                    await Task.Delay(1000);
                    Debug.Log("Task 2  Loop :  " + i);
                }
            });
        }

        private void SetManualResetEvent()
        {
            if (IsReset)
                manualResetEvent.Reset();
            else
                manualResetEvent.Set();

            IsReset = !IsReset;
        }

        如上述代码,Task 1 和 Task 2 都可以被暂时挂起等待。而我们设置的 manualResetEvent 可以同时管理这两个线程任务的等待。

6.4、WaitHandle

        WaitHandle 是继承 MarshalByRefObject 的抽象类,用于同步应用程序中的线程。调用 WaitHandle 类的任何方法都可以阻塞线程,而释放线程取决于选择的 Signaling 构造的类型。

WaitHandle 类 (System.Threading) | Microsoft Learn封装等待对共享资源进行独占访问的操作系统特定的对象。https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.waithandle?view=netstandard-2.1

        其实在之前章节我们已经用过 WaitHandle 了,AutoResetEvent 和 ManualResetEvent 都是继承自 WaitHandle 。通过之前章节的使用,相信大家已经知道这个类的基本用法了。这里单独说一下 WaitHanlde 的两个静态方法:

  • WaitAll:线程等待数组中的所有等待句柄收到信号。

  • WaitAny:线程等待指定一组等待句柄中的任何一个被发出信号。

        我们写一段示例代码:

        public static void RunWithWaitHandle()
        {
            AutoResetEvent[] waitHandles =
            {
                new AutoResetEvent(false),
                new AutoResetEvent(false),
            };

            RandomTimeToSet(waitHandles[0]);
            RandomTimeToSet(waitHandles[1]);

            WaitHandle.WaitAll(waitHandles);//等待2个任务的信号
            //WaitHandle.WaitAny(waitHandles);//等待2个任务中任意一个信号
            Debug.Log($"RunWithWaitHandle 执行完成 !");
        }
        
        public static void RandomTimeToSet(AutoResetEvent handle)
        {
            Task.Run(async () =>
            {
                System.Random random = new System.Random();
                int waitTime = random.Next(1000, 10000);
                Debug.Log($"开始等待 {waitTime} !");
                await Task.Delay(waitTime);
                Debug.Log($"等待 {waitTime} 完成");
                handle.Set();//发出信号
            });
        }

        这里我们用 WaitAll 来测试,结果如下:

【C#】并行编程实战:同步原语(3)_第3张图片

        当然,如果用 WaitAny 则只需要等待其中一个任务发出信号即可。

        这里值得一提的还有一个 API:

  • SignalAndWait:向一个 WaitHandle 发出信号并等待另一个。

        直接看说明比较抽象,这里直接展示代码:

        public static void RunWtihWatiHandleSignalAndWait()
        {
            var autoResetEvent1 = new AutoResetEvent(false);
            var autoResetEvent2 = new AutoResetEvent(false);

            WaitSingalToDebug(autoResetEvent1);
            RandomTimeToSet(autoResetEvent2);

            Task.Run(async () =>
            {
                await Task.Delay(500);
                WaitHandle.SignalAndWait(autoResetEvent1, autoResetEvent2);
                Debug.Log($"RunWtihWatiHandleSignalAndWait 执行完成 !");
            });
        }
        
        public static void WaitSingalToDebug(AutoResetEvent handle) 
        {
            Task.Run(() =>
            {
                Debug.Log("WaitSingalToDebug 开始等待!");
                handle.WaitOne();
                Debug.Log("WaitSingalToDebug 等待完成!");
            });
        }

        执行结果如下:

【C#】并行编程实战:同步原语(3)_第4张图片

         可以看到,SignalAndWait 这一行代码直接相当于同时执行2个操作:Set 和 WaitOne 。立即向 autoResetEvent1 发出信号使得 WaitSingalToDebug 这个任务能够正常执行;同时又等待 RandomTimeToSet 任务的信号以继续。

        WaitHandle 的用处还是很有用的。有时我们并不想等待其他任务执行完成,又需要其他任务提供一个节点以继续运行,此时使用 WaitHandle 会是不错的选择。


        (未完待续)

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

你可能感兴趣的:(多线程编程,C#,学习总结,unity,c#,多线程编程)