C# Events类

与互斥和信号量对象一样,事件也是一个系统范围内的资源同步方法。为了从托管代码中使用系统事件,.NET Framework 在 System.Threading 名称空间中提供了ManualResetEvent、AutoResetEvent、ManualResetEventSlim 和 CountdownEvent 类。.

注意:

C#中的 event 关键字,它与 System.Threading 名称空间中的 event 类没有关系。event 关键字基于委托,而上述两个 event 类是 .NET 封装器,用于系统范围内的本机事件资源的同步。

可以使用事件通知其他任务:这里有一些数据,并完成了一些操作等。事件可以发信号,也可以不发信号。使用前面介绍的 WaitHandle 类,任务可以等待处于发信号状态的事件。

调用 Set() 方法,即可向 ManualResetEventSlim 发信号。调用 Reset() 方法,可以使之返回不发信号的状态。如果多个线程等待向一个事件发信号,并调用了Set() 方法,就释放所有等待的线程。另外,如果一个线程刚刚调用了 WaitOne()方法,但事件已经发出信号,等待的线程就可以继续等待。

也通过调用 Set() 方法向 AutoResetEvent 发信号。也可以使用 Reset() 方法使之返回不发信号的状态。但是,如果一个线程在等待自动重置的事件发信号,当第一个线程的等待状态结束时,该事件会自动变为不发信号的状态。这样,如果多个线程在等待向事件发信号,就只有一个线程结束其等待状态,它不是等待时间最长的线程,而是优先级最高的线程。

为了说明ManualResetEventSlim类的事件,下面的Calculator 类定义了 Calculation() 方法,这是任务的入口点。在这个方法中,该任务接收用于计算的输入数据,将结果写入变量 result,该变量可以通过 Result 属性来访问。只要完成了计算(在随机的一段时间过后),就调用 ManualResetEventSlim 类的 Set 方法,向事件发信号:

public class Calculator
{
  private ManualResetEventSlim mEvent; 
  public int Result { get; private set; }
  
  public Calculator(ManualResetEventSlim ev)
  {
      _mEvent = ev;
  }
  
  public void Calculation(int x, int y)
  {
    Console.WriteLine($"Task {Task.CurrentId} starts calculation"); 
    Task.Delay(new Random().Next(3000)).Wait(); 
    Result = x + y;
    // signal the event-completed!
    Console.WriteLine($"Task {Task.CurrentId} is ready"); 
     _mEvent.Set();
  }
}

程序的 Main() 方法定义了包含 4 个 ManualResetEventSlim 对象的数组和包含 4个Calculator对象的数组。每个Calculator在构造函数中用一个ManualResetEventSlim 对象初始化,这样每个任务在完成时都有自己的事件对象来发信号。现在使用 Task 类,让不同的任务执行计算任务。

class Program
{
  static void Main()
  {
    const int taskCount = 4;
    var mEvents = new ManualResetEventSlim[taskCount]; 
    var waitHandles = new WaitHandle[taskCount]; 
    var calcs = new Calculator[taskCount]; 
    for (int i = 0; i < taskCount; i++)
    {
      int il = i;
      mEvents[i] = new ManualResetEventSlim(false);
      waitHandles[i] = mEvents[i].WaitHandle; 
      calcs [i] = new Calculator(mEvents[i]);
      Task.Run(() => calcs[il].Calculation(il + 1, il + 3));
    }
//...

WaitHandle 类现在用于等待数组中的任意一个事件。WaitAny() 方法等待向任意一个事件发信号。与 ManualResetEvent 对象不同,ManualResetEventSlim 对象不派生自WaitHandle类。因此有一个WaitHandle对象的集合,它在ManualResetEventSlim 类的 WaitHandle 属性中填充。从 WaitAny() 方法返回的 index 值匹配传递给 WaitAny() 方法的事件数组的索引,以提供发信号的事件的相关信息,使用该索引可以从这个事件中读取结果。

for (int i = 0; i < taskCount; i++)
{
  int index = WaitHandle.WaitAny(waitHandles); 
  if (index == WaitHandle.WaitTimeout)
  {
    Console.WriteLine("Timeout!!");
  }
  else
  {
    mEvents[index].Reset();
    Console.WriteLine($"finished task for {index}, result:
      {calcs [index].Result}");
  }
}

启动应用程序时,可以看到任务在进行计算并设置事件,以通知主线程,它可以读取结果了。在任意时间,依据是调试版本还是发布版本,以及硬件的不同,会看到执行调用的任务有不同的顺序和不同的数量。

Task 4 starts calculation 
Task 5 starts calculation 
Task 6 starts calculation 
Task 7 starts calculation 
Task 7 is ready
finished task for 3, result: 10 
Task 4 is ready
finished task for 0, result: 4 
Task 6 is ready
finished task for 1, result: 6 
Task 5 is ready
finished task for 2, result: 8

在一个类似的场景中,为了把一些工作分支到多个任务中,并在以后合并结果,使用新的 CountdownEvent 类很有用。不需要为每个任务创建一个单独的事件对象,而只需要创建一个事件对象。CountdownEvent 类为所有设置了事件的任务定义一个初始数字,在到达该计数后,就向 CountdownEvent 类发信号。

修改 Calculator 类,以使用 CountdownEvent 类替代 ManualResetEvent 类。不使用 Set() 方法设置信号,而使用 CountdownEvent 类定义 Signal() 方法。

public class Calculator
{
  private CountdownEvent _cEvent;
  public int Result { get; private set; }
  
  public Calculator(CountdownEvent ev)
  {
    _cEvent = ev;
  }
  
  public void Calculation(int x, int y)
  {
    Console.WriteLine($"Task {Task.CurrentId} starts calculation"); 
    Task.Delay(new Random().Next(3000)).Wait(); 
    Result = x + y;
    // signal the event-completed!
    Console.WriteLine($"Task {Task.CurrentId} is ready"); 
    _cEvent.Signal();
  }
}

Main() 方法现在可以简化,使它只需要等待一个事件。如果不像前面那样单独处理结果,这个新版本就很不错。

const int taskCount = 4;
var cEvent = new CountdownEvent(taskCount);
var calcs = new Calculator[taskCount]; 
for (int i = 0; i < taskCount; i++)
{
  calcs[i] = new Calculator(cEvent); 
  int il = i;
  Task.Run(() => calcs[i1].Calculation, Tuple.create(i1 + 1, i1 + 3));
}
cEvent.Wait();

Console.WriteLine("all finished"); 
for (int i = 0; i < taskCount; i++)
{
  Console.WriteLine($"task for {i}, result: {calcs[i].Result}");
}