与互斥和信号量对象一样,事件也是一个系统范围内的资源同步方法。为了从托管代码中使用系统事件,.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}");
}