C#线程:使用事件等待句柄发送信号

最简单的信号发送结构是事件等待句柄(注意它和C#的事件是无关的)。事件等待句柄有三种实现:AutoResetEventManualResetEvent(Slim)CountdownEvent。前两种基于通用的EventWaitHandle类,它们继承了基类的所有功能。

AutoResetEvent

AutoResetEvent就像验票机的闸门一样:一张票据只允许一人通过。其名称中的Auto指的是开放的闸机在行人通过后会自动关闭或重置。线程可以调用WaitOne方法在闸机门口等待、阻塞。调用Set方法即向闸机中插入一张票据。如果有一系列的线程调用了WaitOne,那么它们会在闸机后排队等待。票据可以来自任何线程,即任何一个能够访问AutoResetEvent对象的非阻塞线程都可以调用Set方法来释放一个阻塞的线程。

创建AutoResetEvent的方法有两种。
第一种是使用其构造器(传true相当于立刻调用Set方法):

var auto = new AutoResetEvent(false);

第二种方法则是使用如下方式创建AutoResetEvent:

var auto = new EventWaitHandle(false, EventResetMode.AutoReset);

在下面的例子中,当线程启动后就开始等待,直至另一个线程发送信号:

static EventWaitHandle _waitHandle = new AutoResetEvent(false);
static void Waiter()
{
    Console.WriteLine("Waiting...");
    _waitHandle.WaitOne();  // 等待通知
    Console.WriteLine("Notified");
}
static void Main(string[] args)
{
    new Thread(Waiter).Start();
    Thread.Sleep(1000);
    _waitHandle.Set(); // 唤醒等待.

    Console.ReadKey();
}

image

在没有任何线程等待的情况下调用Set方法会导致句柄一直处于打开状态,直至有线程调用了WaitOne方法。

在AutoResetEvent对象上调用Reset方法可以无须等待或阻塞就关闭闸机的门(若原本处于开启状态的话)。
而WaitOne可以接受一个可选的超时参数。如果在超时时间内没有收到信号,则返回false。

双向信号

假设主线程需要向工作线程连续发送三次信号。如果主线程单纯地连续调用Set方法若干次,那么第二次或第三次发送的信号就有可能丢失,因为工作线程需要时间来处理每一次的信号。
其解决方案是主线程等待工作线程准备就绪之后再发送信号。这可以通过引入另一个AutoResetEvent来实现,例如:

static EventWaitHandle _ready = new AutoResetEvent(false);
static EventWaitHandle _go = new AutoResetEvent(false);
static readonly object _locker = new object();
static string _message;

static void Work()
{
    while (true)
    {
        _ready.Set();  // Indicate that we're ready
        _go.WaitOne(); // Wait to be kicked off...
        lock (_locker)
        {
            if (_message == null) 
                return;        // Gracefully exit
            Console.WriteLine(_message);
        }
    }
}

static void Main(string[] args)
{

    new Thread(Work).Start();

    _ready.WaitOne();       // First wait until worker is ready
    lock (_locker)
        _message = "ooo";
    _go.Set();              // Tell worker to go

    _ready.WaitOne();
    lock (_locker) 
        _message = "ahhh";  // Give the worker another message
    _go.Set();

    _ready.WaitOne();
    lock (_locker)
        _message = null;    // Signal the worker to exit
    _go.Set();
    Console.ReadKey();
}

上述程序的执行过程如图描述:
image

ManualResetEvent

ManualResetEvent的作用就像是一个大门。调用Set方法就开启大门,并允许任意数量的调用WaitOne方法的线程通过大门。而调用Reset方法则会关闭大门。在大门关闭时调用WaitOne方法会发生阻塞。而当大门再次打开时,线程会立刻释放。除这些区别之外,ManualResetEvent的功能和AutoResetEvent是一样的。
ManualResetEvent适用于用一个线程来释放其他所有线程的情形,而CountdownEvent则适用于相反的情形。

CountdownEvent

CountdownEvent类可用于等待多个线程,它具有高效的纯托管实现。实例化该类时,需要指定线程数或者需要等待的“计数”:

var countdown = new CountdownEvent(3);

调用Signal会使计数递减;而调用Wait则会阻塞,直至计数减为零。

static CountdownEvent _countdown = new CountdownEvent(3);
static void SaySomething(object thing)
{
    Thread.Sleep(1000);
    Console.WriteLine(thing);
    _countdown.Signal();
}

static void Main(string[] args)
{
    new Thread(SaySomething).Start("线程 1");
    new Thread(SaySomething).Start("线程 2");
    new Thread(SaySomething).Start("线程 3");
    _countdown.Wait();   // 阻塞 直到信号被调用3次
    Console.WriteLine("所有线程都完成!");

    Console.ReadKey();
}

调用AddCount方法可以重新增加CountdownEvent的计数。但是如果它的计数已经降为零,则调用该方法会抛出异常:我们无法通过调用AddCount来取消CountdownEvent的信号。为了避免抛出异常,还可以使用TryAddCount。若计数值为0,则该方法会返回false。
调用Reset方法可以取消计数事件的信号:它不但取消信号,而且会将计数值重置为原始设定值。
和ManualResetEventSlim相似,CountdownEvent也有一个WaitHandle属性以便支持其他依赖WaitHandle的类或者方法。

等待句柄和延续操作

如果不希望等待一个句柄从而阻塞线程,还可以调用ThreadPool.RegisterWaitForSingleObject方法将一个延续操作附加在等待句柄上。该方法接受一个委托对象,并会在句柄收到信号时执行:

static ManualResetEvent _starter = new ManualResetEvent(false);
public static void Go(object data, bool timedOut)
{
    Console.WriteLine("Started - " + data);
    // Perform task...
}
static void Main(string[] args)
{
    RegisteredWaitHandle reg = ThreadPool
        .RegisterWaitForSingleObject(_starter, Go, "Some Data", -1, true);
    Thread.Sleep(5000);
    Console.WriteLine("Signaling worker...");
    _starter.Set();
    Console.ReadLine();
    reg.Unregister(_starter);    // 结束后的清理操作

    Console.ReadKey();
}

当等待句柄接到信号时,委托就会在一个线程池线程中执行。之后,还需要调用Unregister解除非托管句柄和回调之间的关系。
除了等待句柄和委托之外,RegisterWaitForSingleObject方法还可以接收一个“黑盒”对象,并将其作为参数传递给委托方法。此外,还可以指定一个以毫秒为单位的超时时间(若不需要超时,则指定-1)以及一个布尔类型的标记以确定该请求的执行是一次性的还是会重复发生。

WaitAny、WaitAll和SignalAndWait

除了Set、WaitOne和Reset方法,WaitHandle类还具有一些执行复杂同步操作的静态方法。其中WaitAny、WaitAll和SignalAndWait方法可以对多个句柄执行信号发送或者等待操作。具体的等待句柄可以是不同类型的。对于ManualResetEventSlim和CountdownEvent也可以在其WaitHandle属性上使用这些方法。
WaitHandle.WaitAny可以等待一组句柄中的任意一个句柄;
WaitHandle.WaitAll可以用原子的方式等待所有给定的句柄。
因此,如果等待两个AutoResetEvent对象:

  • WaitAny方法无法在最终同时“控制”两个事件。
  • WaitAll方法无法在最终只“控制”其中一个事件。

SignalAndWait方法会调用其中一个WaitHandle的Set方法,而后调用另一个WaitHandle的WaitOne方法。在第一个等待句柄信号发送之后,它会转而跳到等待第二个句柄的队列头部以尽可能地使等待成功(但这个过程并非原子操作)。这个方法就像从一个信号“切换”到另一个信号。如果在一对EventWaitHandle上使用该方法就可以令两个线程在同一时刻汇合。AutoResetEvent和ManualResetEvent都可以完成这种操作。
第一个线程执行以下代码:

WaitHandle.SignalAndWait(wh1,wh2);

另一个线程执行相反的操作:

WaitHandle.SignalAndWait(wh2,wh1);
posted @ 2022-09-01 10:38  一纸年华  阅读(1324)  评论(0编辑  收藏  举报