本节内容相对来说比较基础,但也是为了后面的文章做铺垫,因此个人觉得有必要单独拿出来写一写。
一、线程间的信号传递
我们知道,线程之间的关系是异步的,谁也不干预谁。但在实际执行过程中,有时需要线程可以做到同步,即一方等待另一方的通知,再继续执行下一步。
这时候,就需要有一个东西,既能够阻止等待方的执行,又能够让另一方执行到某一处时告知前者,“我已执行完毕,你可以继续了”。于是,微软便提供了这样一个对象,叫做WaitHandle。它是互斥量和同步信号量的基元,也是一个抽象类,因此我们使用它时要根据情况使用它派生的各个子类。
下图展示了WaitHandle是如何进行工作的:
二、线程同步事件(EventWaitHandle)
EventWaitHandle是最基础的线程同步事件类,AutoResetEvent和ManualResetEvent都是派生自此类,不过仅仅外包了一层构造函数,并没有增加其他的方法和属性。不仅没有增加,反而隐藏了给事件命名的构造方法。要知道,基于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。
好了,说完以上内容,其实对同步锁的概念也有了初步的认识,下一节的内容会更容易理解。