.NET 多线程开发总结(三)——线程间的信号传递(线程交互)

本节内容相对来说比较基础,但也是为了后面的文章做铺垫,因此个人觉得有必要单独拿出来写一写。

一、线程间的信号传递

我们知道,线程之间的关系是异步的,谁也不干预谁。但在实际执行过程中,有时需要线程可以做到同步,即一方等待另一方的通知,再继续执行下一步。
这时候,就需要有一个东西,既能够阻止等待方的执行,又能够让另一方执行到某一处时告知前者,“我已执行完毕,你可以继续了”。于是,微软便提供了这样一个对象,叫做WaitHandle。它是互斥量和同步信号量的基元,也是一个抽象类,因此我们使用它时要根据情况使用它派生的各个子类。
下图展示了WaitHandle是如何进行工作的:

在这里插入图片描述
二、线程同步事件(EventWaitHandle)

EventWaitHandle是最基础的线程同步事件类,AutoResetEventManualResetEvent都是派生自此类,不过仅仅外包了一层构造函数,并没有增加其他的方法和属性。不仅没有增加,反而隐藏了给事件命名的构造方法。要知道,基于WaitHandle的对象都是可以在操作系统中访问到的,也就是可以跨进程访问,如果不能给对象命名,那么这一功能将会丧失。(可能微软也不希望这么用,因为多个线程同时等待一个信号时,无法决定谁先接收到信号,如果用了AutoReset模式,这将会很糟糕【只有一个线程能收到,而且不确定谁会收到= =】)

所以接下来我打算只介绍EventWaitHandle最常见的使用:

1、构造
关于EventWaitHandle的构造函数,有多个重载,比较常用的构造函数如下

public EventWaitHandle(bool initialState, EventResetMode mode);

第一个参数表示是否在初始时将状态设置为终止状态,说得直白点就是设置最开始是否为收到信号的状态,如果是,则接下来的第一个等待将不会阻止线程;
第二个参数表示设置事件重置的方式,也就是当收到信号时,是否自动把已接收状态变为未接收状态。这里如果设为True,就相当于AutoResetEvent;如果是False,就相当于ManualResetEvent。

2、常用方法
Set():将线程同步事件标记为有信号,此时处于阻塞等待状态的线程将会停止等待。如果模式为AutoReset,那么还会自动触发Reset()方法。
Reset():将线程同步事件标记为无信号。
WaitOne():阻止当前线程,直到收到信号。该方法的另外一些重载,可以允许设置等待超时时间,并决定是否在等待前退出当前上下文(关于上下文,会在文字后面补充说明)
WaitAny():等待一个同步事件数组中任意一个元素收到信号
WaitAll():等待一个同步事件数组中所有元素都收到信号
SignalAndWait():该方法会传入两个同步事件对象,作用是给前者发出信号,再等待后者收到信号

这里要特别说明最后一个方法,因为它比较特殊。前面几种等待方法,都是一个线程等待另一个或几个线程的信号,再继续做自己的事。而最后一个方法,则运用在两个线程需要互相等待的情况。
下面举个栗子:
在这里插入图片描述

如图所示,A、B线程并行执行过程1,但B要执行过程2需等待A执行完过程1发出信号,A要执行过程2需等待B执行完过程2发出信号。如此实现了一个 A.过程1—>B.过程2—>A.过程2 的一个同步。
下面上代码:

static void Main(string[] args)
{
    EventWaitHandle eventWaitA = new EventWaitHandle(false, EventResetMode.AutoReset);
    EventWaitHandle eventWaitB = new EventWaitHandle(false, EventResetMode.AutoReset);

    Task.Factory.StartNew(() =>
    {
        Console.WriteLine("线程A.过程1:开始执行");
        Thread.Sleep(2000);
        Console.WriteLine("线程A.过程1:执行完毕");

        EventWaitHandle.SignalAndWait(eventWaitA, eventWaitB);

        Console.WriteLine("\n=============================================");
        Console.WriteLine("线程A.过程2:开始执行");
        Thread.Sleep(1000);
        Console.WriteLine("线程A.过程2:执行完毕");
        Console.WriteLine("=============================================");
    });

    Task.Factory.StartNew(() =>
    {
        Console.WriteLine("线程B.过程1:开始执行");
        Thread.Sleep(1000);
        Console.WriteLine("线程B.过程1:执行完毕");

        eventWaitA.WaitOne();

        Console.WriteLine("\n=============================================");
        Console.WriteLine("线程B.过程2:开始执行");
        Thread.Sleep(2000);
        Console.WriteLine("线程B.过程2:执行完毕");
        Console.WriteLine("=============================================");

        eventWaitB.Set();
    });
}

在这里插入图片描述
以上代码无论怎样运行,都一定是:
A.过程1执行完毕,B.过程2才开始执行;
B.过程2执行完毕,A.过程2才开始执行。


正文说完了,下面来说一说本节的附加内容:同步上下文

上下文指的是对象集合所处的环境,它是在对象激活过程中建立的。因此,要想实现上下文同步,必须是对非静态对象进行操作。
关于上下文的理解,是比较抽象的,需要结合大量的试验操作来明确它的用法。这里就仅展示如何创建一段同步上下文区域,来实现线程间同步的:

/// <summary>
/// 创建一个同步上下文对象
/// </summary>
[Synchronization(true)]     // 此特性用于对上下文提供同步操作,参数reEntrant表示需要重入(待会会讲哪里需要重入)
public class SyncContent : ContextBoundObject   // 定义上下文对象必须基于此类
{
    public EventWaitHandle eventWait = new EventWaitHandle(false, EventResetMode.AutoReset);
    public void DoWork(object exitContext)
    {
        Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}进入同步上下文方法——{DateTime.Now.ToString("HH:mm:ss")}");
        if (!eventWait.WaitOne(3000, (bool)exitContext))
        {
            Console.WriteLine("等待超时");
        };
        Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}退出同步上下文方法——{DateTime.Now.ToString("HH:mm:ss")}");
    }

    public void Signal()
    {
        eventWait.Set();
        Console.WriteLine("给线程发送消息");
    }
}

static void Main(string[] args)
{
    SyncContent syncContent = new SyncContent();
    Task.Factory.StartNew((obj) => syncContent.DoWork(obj), false);
    Thread.Sleep(500);
    syncContent.Signal();
}

正常来说,我创建了一个线程去执行WaitHandle等待操作,再由主线程给它发送信号,理论上是能立即退出等待的。但实际执行的结果却是下面这样的:
在这里插入图片描述
任务线程直到超时退出后,主线程才执行发送消息的方法,说明在任务执行过程中,整个SyncContent对象都被锁住了,于是造成主线程被阻塞。而这就是同步上下文的意义,它能使一块代码区域成为同步域,一次只能由一个线程进入。

那么有没有特例呢?
当然有,那就是我们前面提到的重入的概念。
打个比方,我们去快餐店吃饭,点餐的过程是需要排队的。假设你排到了点餐口,突然发现自己没带钱,你打电话求助。如果你一直占着点餐口,那么后面的人肯定会有意见。所以允许你先到一边,等钱来了再回到点餐口付钱。
先退出点餐口(同步域),等待钱到达(WaitOne),再回到点餐口(同步域)付钱,这个过程就叫重入。
不过,你可以自己决定要不要重入,你也可以一直占着点餐口,让后面的人一直等。这个决定标志,就是WaitOne的后面一个参数exitContext。

好了,说完以上内容,其实对同步锁的概念也有了初步的认识,下一节的内容会更容易理解。

上一篇:.NET 多线程开发总结(二)——Thread、ThreadPool、Task、Parallel的简单使用

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在实际应用中,.NET多线程可以提供一些好的案例。以下是一些典型的案例: 1. 并行计算:对于计算密集型任务,多线程可以将任务分解为多个子任务,并在多个线程上并行执行。这样可以大大提高计算的速度和效率。例如,在图像处理中,可以将图像分成多个区块,每个区块由一个线程处理。 2. 多线程网络通信:网络应用程序中,可以通过多线程来处理多个客户端的请求。每个线程可以处理一个客户端的请求,不同线程可以独立运行,并发地处理多个请求,提高了服务器的吞吐量和性能。 3. 数据库操作:当需要对数据库进行大量数据处理时,使用多线程可以将任务分解为多个子任务,并在多个线程上并行执行。例如,在数据分析场景下,可以将大量数据分为多个批次,使用多线程并行地执行数据库操作,提高了查询和处理的效率。 4. 用户界面响应:在开发用户界面时,使用多线程可以提高界面的响应速度。例如,在界面上执行长时的操作时,可以将该操作放在一个单独的后台线程中执行,而不阻塞界面线程,从而使界面保持流畅的交互性。 5. 异步编程:使用多线程可以实现异步编程模型,提高程序的并发度和性能。例如,通过多线程可以实现并行的文件读写、网络请求等操作,提高了程序的运行效率。 总的来说,.NET多线程可以应用在各种场景下,提高了系统的并发处理能力和响应速度,有效地利用计算资源,提高了系统的性能和用户体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值