专栏首页喵叔's 专栏线程同步 (二)

线程同步 (二)

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

  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 的使用率明显降低并接近于平时的使用率。

四、总结

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

五、代码下载

代码下载

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 线程基础必知必会(二)

    这篇文章将在上篇文章的基础上,进一步讲解线程的相关知识。这篇文章涉及到的知识点有 线程优先级、前台与后台线程、线程参数、lock、Monitor 和 线程异常处...

    喵叔
  • 线程同步(一)

    当多个线程同时对同一个内存地址进行写入时,由于CPU时间调度上的问题写入数据会被多次的覆盖,所以就要使线程同步。所谓的同步就是协同步调,按预定的先后次序进行运行...

    喵叔
  • C# 监控 Windows 文件夹

    您是否为无法看到孩子在电脑上存储的图片而发愁,您是否为无法监控员工在电脑上存储的东西而发愁,那么今天给您推荐的这款产品绝对是您不二的选择,它是由美国大厂生产,完...

    喵叔
  • 代码编排架构三部曲简述

    本文将基于三种常见的编码处理场景,介绍 Event Reactive 的代码编排架构。

    heidsoft
  • 代码编排架构三部曲简述

    本文将基于三种常见的编码处理场景,介绍 Event Reactive 的代码编排架构。

    用户1516716
  • 写了10年JAVA代码,为何还是给人一种乱糟糟的感觉?

    接触过不少号称写了10多年代码的程序员,可经常还是会发现他们的代码给人一种乱糟糟的感觉,那么如何才能写出让同事感觉不那么乱的代码呢?

    用户5927304
  • 科学家发现新的3D打印材料,可打印人的皮肤 | 黑科技

    镁客网
  • 安卓开发_深入理解Handler消息传递机制

    听着music睡
  • python 中的scipy模块

    润森
  • 在我们的shiny服务器再部署个芯片下游分析网页工具

    但是它并不提供芯片探针的ID注释,当然,缺陷实在是有点多,只能说是一个好的学习shiny网页工具制作的例子,并不算是完善的工具。

    生信技能树

扫码关注云+社区

领取腾讯云代金券