线程同步 (二)

时间:2022-07-24
本文章向大家介绍线程同步 (二),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

下面我们接着讲线程同步相关的知识点,本节主要讲解以下四小节的内容:

  1. CountDownEvent
  2. Barrier
  3. ReaderWriterLockSlim
  4. SpinWait

零、CountDownEvent

CountdownEvent 是一个同步基元,它在收到一定次数的信号之后,将会解除对其等待线程的锁定。 一般用于必须使用 ManualResetEvent 或 ManualResetEventSlim 并且必须在用信号通知事件之前手动递减一个变量的情况,简单的说就是主要用在需要等待多个异步操作完成的情况。

using System;
using System.Threading;
using static System.Console;
using static System.Threading.Thread;


namespace CountDownEventClass
{
    class Program
    {
        static void Main(string[] args)
        {
            WriteLine("您好,您吃点什么?");
            Thread tomThread = new Thread(() => Order("鱼香肉丝、红烧茄子、凉拌腐竹"));
            tomThread.Name = "Tom";
            Thread jackThread = new Thread(() => Order("猪肉大葱饺子、紫菜蛋花汤"));
            jackThread.Name = "Jack";
            Thread cookThreaf = new Thread(()=>Cook());
            tomThread.Start();
            jackThread.Start();
            cookThreaf.Start();

            Read();
        }
        static CountdownEvent countDownEvent = new CountdownEvent(2);
        static void Order(string order)
        {
            WriteLine($"{CurrentThread.Name} 开始点餐");
            Random ran = new Random();
            int sleep = ran.Next(1000, 10000);
            Sleep(sleep);
            countDownEvent.Signal();
            WriteLine($"{CurrentThread.Name} 点餐完毕,我要吃 {order}");
        }
        static void Cook()
        {
            WriteLine("大厨等待做饭....");
            countDownEvent.Wait();
            WriteLine("大厨开始做饭");
            Random ran = new Random();
            int sleep = ran.Next(1000, 5000);
            Sleep(sleep);
            WriteLine("大厨做完饭了");
            countDownEvent.Dispose();
        }
    }
}

在上面的代码中我们模拟了一个去饭店吃饭点餐大厨做饭的简单流程。首先我们创建了一个 CountdownEvent 实例,并指定会有两个操作完成时发出信号量。接着我们创建了 OrderCook 方法,分别来模拟点餐和下单。我们在 Cook 方法中调用了 Wait 方法是等待两个信号量的发出,当两个操作完成并都发出信号量时会继续执行后面的代码。同样我们在 Order 方法中调用了 Signal 方法,用来在操作完成后发出信号量。运行结果如下图:

Tip:这里需要注意的是 如果 调用 Signal()没达到指定的次数,那么 Wait() 将一直等待,因此这里要明确有多少个操作会在执行后需要发出信号量,并且要保证每次操作完成后都要调用 Signal() 方法。

一、Barrier

Barrier 是一个很有意思的类,他和 CountDownEvent 类的功能类似,只不过比它多了一个回调函数,这个回调函数会在每个线程完成一节阶段后调用。 Barrier 类经常被用在多线程迭代运算中,用来控制每个线程的阶段。

using System;
using System.Threading;
using static System.Threading.Thread;
using static System.Console;
using System.Collections.Generic;

namespace BarrierClass
{
    class Program
    {
        static void Main(string[] args)
        {
            List<string> names = new List<string>
            {
                "Jack",
                "Tome",
                "Rose",
                "Sun"
            };
            foreach (string name in names)
            {
                Thread thread = new Thread(() => Work());
                thread.Name = name;
                thread.Start();
            }
            Read();
        }
        static Barrier barrier = new Barrier(4, Publish);

        private static void Publish(Barrier b)
        {
            WriteLine($"{b.ParticipantCount} 名开发人员全部开发完成项目的第 {b.CurrentPhaseNumber + 1} 期,开始发布上线!");
            if (b.CurrentPhaseNumber + 1 == 3)
            {
                WriteLine("项目全部开发完成!");
            }
        }
        static void Work()
        {
            WriteLine($"{CurrentThread.Name} 完成第 1 期开发");
            barrier.SignalAndWait();
            WriteLine($"{CurrentThread.Name} 完成第 2 期开发");
            barrier.SignalAndWait();
            WriteLine($"{CurrentThread.Name} 完成第 3 期开发");
            barrier.SignalAndWait();
        }
    }
}

在上面的代码中我们模拟了项目开发的流程。我们首先定义了一个 Barrier 类的实例,并指定了 4 个需要同步的线程,每个线程都会在调用 SignalAndWai 方法后去调用回调函数 Publish 。这个类在多线程迭代运算中非常有用,我们可以在每个迭代结束前执行一些计算。当最后一个线程调用 SignalAndWait 方法时可以执行一些特殊的操作。

二、ReaderWriterLockSlim

ReaderWriterLockSlim 类会创建线程安全机制,它允许多个线程读取的同时只有一个线程独占资源。我们一般使用 ReaderWriterLockSlim 来保护由多个线程读取但每次只采用一个线程写入的资源。 ReaderWriterLockSlim 允许多个线程均处于读取模式,允许一个线程处于写入模式并独占锁定状态,同时还允许一个具有读取权限的线程处于可升级的读取模式,在此模式下线程无需放弃对资源的读取权限即可升级为写入模式。

using System;
using System.Threading;
using static System.Threading.Thread;
using static System.Console;
using System.Collections.Generic;
using System.Linq;

namespace ReaderWriterLockSlimClass
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread thread1 = new Thread(Read);
            thread1.Start();
            Thread thread2 = new Thread(Read);
            thread2.Start();
            Thread thread3 = new Thread(Read);
            thread3.Start();
            Thread thread4 = new Thread(Write);
            thread4.Name = "Write1";
            thread4.Start();
            Thread thread5 = new Thread(Write);
            thread5.Name = "Write2";
            thread5.Start();
            Read();
        }
        static ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim();
        static List<int> items = new List<int>();
        static void Read()
        {
            WriteLine("开始读取 List");
            while(true)
            {
                try
                {
                    lockSlim.EnterReadLock();
                    foreach (int item in items)
                    {
                        WriteLine($"{CurrentThread.Name} 读取 {item}");
                    }
                }
                finally
                {
                    lockSlim.ExitReadLock();
                }
            }
        }

        static void Write()
        {
            while(true)
            {
                try
                {
                    lockSlim.EnterUpgradeableReadLock();
                    int num = new Random().Next(100);
                    WriteLine($"{CurrentThread.Name} 写入 {num}");
                    if (!items.Any(p=>p==num))
                    {
                        try
                        {
                            lockSlim.EnterWriteLock();
                            items.Add(num);
                        }
                        finally
                        {
                            lockSlim.ExitWriteLock();
                        }
                    }
                }
                finally
                {
                    lockSlim.ExitUpgradeableReadLock();
                }
            }
        }
    }
}

在上面的代码中我们创建了 5 个线程,其中 3 个线程用来读取数据,而另 2 个线程用来写入数据。这里使用两种锁:读锁允许多线程读取数据,写锁在被释放前会阻塞了其他线程的所有操作。当一旦得到写锁,会阻止阅读者读取数据,进而浪费大量的时间,因此获取写锁后集合会处于阻塞状态。如果要减少阻塞浪费的时间,我们可以使用 EnterUpgradeableReadLockExitUpgradeableReadLock 方法。先获取读锁后读取数据,如果发现必须修改数据,就使用 EnterWriteLock 方法升级锁,然后执行一次写操作后使用 ExitWriteLock 释放写锁。

三、SpinWait

SpinWait 类是一个混合同步构造,使用用户模式等待一段时间然后切换到内核模式以节省CPU时间减少CPU负载。

using System;
using System.Threading;
using static System.Console;
using static System.Threading.Thread;

namespace SpinWaitClass
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread thread1 = new Thread(UserMode);
            Thread thread2 = new Thread(Spinwait);
            thread1.Start();
            Sleep(20000);
            isCompleted = true;
            Sleep(1000);
            WriteLine("-----------------------------------------------------");
            isCompleted = false;
            thread2.Start();
            Sleep(50000);
            isCompleted = true;
        }
        static volatile bool isCompleted = false;
        static void UserMode()
        {
            while(!isCompleted)
            {
                Write("!!!!!!");
            }
        }

        static void Spinwait()
        {
            SpinWait spinWait = new SpinWait();
           while(!isCompleted)
            {
                spinWait.SpinOnce();
                WriteLine(spinWait.NextSpinWillYield);
            }
        }
    }
}

运行上述代码,我们通过任务管理器中的 CPU 使用情况可以看出当程序开始输出 ! 号时 CPU 的使用率明显变高了,但是在运行 20 秒后,切换到 SpinWait 下运行 CPU 的使用率明显降低并接近于平时的使用率。

四、总结

通过两篇文章讲解线程同步,希望大家可以理解其中的内容,在多线程开发中我们可以根据不同的场景使用不同的线程同步的方法或者这些方法的组合。

五、代码下载

代码下载