目录
介绍
像C#这样的现代语言集成了事件机制,它实际上将观察者模式集成到语言机制中。
实际上,事件机制提供同步调用这一事实经常被忽视,并且没有得到足够的重视。程序员经常有并行的错觉,这不是现实,是当今多核处理器世界中的一个重要问题。接下来,我们提供多线程问题的分析和解决方案。
提供的代码是一个教程,概念演示级别,为简洁起见,不处理或显示所有变体/问题。
事件机制在单个线程上提供同步调用
需要强调的是,在调用中:
if (SubjectEvent != null)
{
SubjectEvent(this, args);
}
//or
SubjectEvent?.Invoke(this, args);
正在单个线程上同步调用订阅的EventHandler。这有一些不太明显的后果:
- 按订阅事件的顺序依次执行EventHandler。
- 这意味着早期订阅EventHandler的对象/值比其他订阅EventHandler的对象/值更新得更早,这可能会对程序逻辑产生影响。
- 调用某些EventHandler将阻塞线程,直到完成其EventHandler中的所有工作。
- 如果在某个EventHandler中抛出异常,则在该异常之后订阅的所有EventHandler将不会执行。
我们将在示例中演示它。计划是创建三个EventHandler,每个EventHandler需要10秒才能完成,并监视每个运行线程的线程以及所花费的总时间。我们将输出与此示例相关的每个线程ThreadId,以查看正在使用的线程数。
public class EventArgsW : EventArgs
{
public string StateW = null;
}
public class EventWrapper
{
public event EventHandler<EventArgsW> EventW;
public string StateW;
public void Notify()
{
Console.WriteLine("Notify is running on ThreadId:{0}",
Thread.CurrentThread.ManagedThreadId);
EventArgsW args = new EventArgsW();
args.StateW = this.StateW;
EventW?.Invoke(this, args);
}
}
public class HandlerWrapper
{
private string name;
private string StateW;
private ManualResetEvent mrs;
public HandlerWrapper(string name, ManualResetEvent mrs)
{
this.name = name;
this.mrs = mrs;
}
public void Handler(object subject, EventArgsW args)
{
Console.WriteLine("Handler{0} is running on ThreadId:{1}",
name, Thread.CurrentThread.ManagedThreadId);
Worker(subject, args);
}
private void Worker(object subject, EventArgsW args)
{
Console.WriteLine("Handler{0}.Worker is running on ThreadId:{1}, i:0",
name, Thread.CurrentThread.ManagedThreadId);
StateW = args.StateW;
for (int i = 1; i <= 2; ++i)
{
Thread.Sleep(5000);
Console.WriteLine("Handler{0}.Worker is running on ThreadId:{1}, i:{2}",
name, Thread.CurrentThread.ManagedThreadId, i);
}
mrs.Set();
}
}
internal class Client
{
public static void Main(string[] args)
{
Console.WriteLine("Client is running on ThreadId:{0}",
Thread.CurrentThread.ManagedThreadId);
ManualResetEvent[] mres = new ManualResetEvent[3];
for (int i = 0; i < mres.Length; i++) mres[i] = new ManualResetEvent(false);
EventWrapper s = new EventWrapper();
s.EventW += (new HandlerWrapper("1", mres[0])).Handler;
s.EventW += (new HandlerWrapper("2", mres[1])).Handler;
s.EventW += (new HandlerWrapper("3", mres[2])).Handler;
// Change subject state and notify observers
s.StateW = "ABC123";
var timer = new Stopwatch();
timer.Start();
s.Notify();
ManualResetEvent.WaitAll(mres);
timer.Stop();
TimeSpan timeTaken = timer.Elapsed;
string tmp1 = "Client time taken: " + timeTaken.ToString(@"m\:ss\.fff");
Console.WriteLine(tmp1);
Console.ReadLine();
}
}
执行结果为:
从执行结果可以看出,EventHandler一个接一个地运行,全部在thread Id=1上,与客户端运行的线程相同。完成所有工作需要30.059秒。
使用TPL的异步事件
使用任务并行库(TPL),我们可以使我们的EventHandler在单独的线程上异步运行。更重要的是,如果我们想将Client线程从任何工作中释放出来(假设我们的Client是UI线程),我们可以在与Client线程不同的线程上引发Event(调度EventHandler调用)。以下是新的实现:
新的解决方案代码如下所示:
public class EventArgsW : EventArgs
{
public string StateW = null;
}
public class EventWrapper
{
public event EventHandler<EventArgsW> EventW;
public string StateW;
public void Notify()
{
Task.Factory.StartNew(
() => {
Console.WriteLine("Notify is running on ThreadId:{0}",
Thread.CurrentThread.ManagedThreadId);
EventArgsW args = new EventArgsW();
args.StateW = this.StateW;
EventW?.Invoke(this, args);
});
}
}
public class HandlerWrapper
{
private string name;
private string StateW;
private ManualResetEvent mrs;
public HandlerWrapper(string name, ManualResetEvent mrs)
{
this.name = name;
this.mrs = mrs;
}
public void Handler(object subject, EventArgsW args)
{
Console.WriteLine("Handler{0} is running on ThreadId:{1}",
name, Thread.CurrentThread.ManagedThreadId);
Task.Factory.StartNew(
() => Worker(subject, args)); ;
}
private void Worker(object subject, EventArgsW args)
{
Console.WriteLine("Handler{0}.Worker is running on ThreadId:{1}, i:0",
name, Thread.CurrentThread.ManagedThreadId);
StateW = args.StateW;
for (int i = 1; i <= 2; ++i)
{
Thread.Sleep(5000);
Console.WriteLine("Handler{0}.Worker is running on ThreadId:{1}, i:{2}",
name, Thread.CurrentThread.ManagedThreadId, i);
}
mrs.Set();
}
}
internal class Client
{
public static void Main(string[] args)
{
Console.WriteLine("Client is running on ThreadId:{0}",
Thread.CurrentThread.ManagedThreadId);
ManualResetEvent[] mres = new ManualResetEvent[3];
for (int i = 0; i < mres.Length; i++) mres[i] = new ManualResetEvent(false);
EventWrapper s = new EventWrapper();
s.EventW += (new HandlerWrapper("1", mres[0])).Handler;
s.EventW += (new HandlerWrapper("2", mres[1])).Handler;
s.EventW += (new HandlerWrapper("3", mres[2])).Handler;
// Change subject state and notify observers
s.StateW = "ABC123";
var timer = new Stopwatch();
timer.Start();
s.Notify();
ManualResetEvent.WaitAll(mres);
timer.Stop();
TimeSpan timeTaken = timer.Elapsed;
string tmp1 = "Client time taken: " + timeTaken.ToString(@"m\:ss\.fff");
Console.WriteLine(tmp1);
Console.ReadLine();
}
}
执行结果在这里:
从执行结果可以看出,我们看到EventHandler在单独的线程上运行,从执行日志中可以看到并发性,总耗时为10.020秒。
使用TPL的异步事件——扩展方法
由于TPL的使用需要更改现有代码并混淆代码的可读性,因此我创建了一个Extension方法来简化TPL的使用。而不是编写:
EventW?.Invoke(this, args);
有人会这样编写:
EventW?.InvokeAsync<EventArgsW>(this, args);
所有的TPL魔法都会在幕后发生。以下是新解决方案的所有源代码:
public class EventArgsW : EventArgs
{
public string StateW = null;
}
public class EventWrapper
{
public event EventHandler<EventArgsW> EventW;
public string StateW;
public void Notify()
{
Console.WriteLine("Notify is running on ThreadId:{0}",
Thread.CurrentThread.ManagedThreadId);
EventArgsW args = new EventArgsW();
args.StateW = this.StateW;
EventW?.InvokeAsync<EventArgsW>(this, args); //(1)
}
}
public class HandlerWrapper
{
private string name;
private string StateW;
private ManualResetEvent mrs;
public HandlerWrapper(string name, ManualResetEvent mrs)
{
this.name = name;
this.mrs = mrs;
}
public void Handler(object subject, EventArgsW args)
{
Console.WriteLine("Handler{0} is running on ThreadId:{1}",
name, Thread.CurrentThread.ManagedThreadId);
Worker(subject, args);
}
private void Worker(object subject, EventArgsW args)
{
Console.WriteLine("Handler{0}.Worker is running on ThreadId:{1}, i:0",
name, Thread.CurrentThread.ManagedThreadId);
StateW = args.StateW;
for (int i = 1; i <= 2; ++i)
{
Thread.Sleep(5000);
Console.WriteLine("Handler{0}.Worker is running on ThreadId:{1}, i:{2}",
name, Thread.CurrentThread.ManagedThreadId, i);
}
mrs.Set();
}
}
public static class AsyncEventsUsingTplExtension
{
public static void InvokeAsync<TEventArgs> //(2)
(this EventHandler<TEventArgs> handler, object sender, TEventArgs args)
{
Task.Factory.StartNew(() =>
{
Console.WriteLine("InvokeAsync<TEventArgs> is running on ThreadId:{0}",
Thread.CurrentThread.ManagedThreadId);
var delegates = handler?.GetInvocationList();
foreach (var delegated in delegates)
{
var myEventHandler = delegated as EventHandler<TEventArgs>;
if (myEventHandler != null)
{
Task.Factory.StartNew(() => myEventHandler(sender, args));
}
};
});
}
}
internal class Client
{
public static void Main(string[] args)
{
Console.WriteLine("Client is running on ThreadId:{0}",
Thread.CurrentThread.ManagedThreadId);
ManualResetEvent[] mres = new ManualResetEvent[3];
for (int i = 0; i < mres.Length; i++) mres[i] = new ManualResetEvent(false);
EventWrapper s = new EventWrapper();
s.EventW += (new HandlerWrapper("1", mres[0])).Handler;
s.EventW += (new HandlerWrapper("2", mres[1])).Handler;
s.EventW += (new HandlerWrapper("3", mres[2])).Handler;
// Change subject state and notify observers
s.StateW = "ABC123";
var timer = new Stopwatch();
timer.Start();
s.Notify();
ManualResetEvent.WaitAll(mres);
timer.Stop();
TimeSpan timeTaken = timer.Elapsed;
string tmp1 = "Client time taken: " + timeTaken.ToString(@"m\:ss\.fff");
Console.WriteLine(tmp1);
Console.ReadLine();
}
}
这是执行结果:
从执行结果可以看出,我们看到EventHandle在单独的线程上运行,从执行日志中可以看到并发性,总耗时为10.039秒。TPL正在将工作分派给线程池中的线程,可以看到线程Id=4已被使用了两次,可能它提前完成了工作并再次可用于工作。
使用TAP的异步事件
根据它们在C#中的定义方式的性质,EventHandler是任务异步模式(TAP)上下文中的同步函数。如果您希望EventHandler在TAP的上下文中异步,以便您可以在其中等待,您需要实际推出自己的事件通知机制,以支持您的自定义异步EventHandler版本。在[1]中可以看到这种工作的一个很好的例子。出于示例的目的,我修改了该代码,以下是解决方案的新版本:
public class EventArgsW : EventArgs
{
public string StateW = null;
}
public class EventWrapper
{
public event AsyncEventHandler<EventArgsW> EventW;
public string StateW;
public async Task Notify(CancellationToken token)
{
Console.WriteLine("Notify is running on ThreadId:{0}",
Thread.CurrentThread.ManagedThreadId);
EventArgsW args = new EventArgsW();
args.StateW = this.StateW;
await this.EventW.InvokeAsync(this, args, token);
}
}
public class HandlerWrapper
{
private string name;
private string StateW;
private ManualResetEvent mrs;
public HandlerWrapper(string name, ManualResetEvent mrs)
{
this.name = name;
this.mrs = mrs;
}
public async Task Handler(object subject, EventArgsW args,
CancellationToken token)
{
Console.WriteLine("Handler{0} is running on ThreadId:{1}",
name, Thread.CurrentThread.ManagedThreadId);
await Worker(subject, args);
}
private async Task Worker(object subject, EventArgsW args)
{
Console.WriteLine("Handler{0}.Worker is running on ThreadId:" +
"{1}, i:0",
name, Thread.CurrentThread.ManagedThreadId);
StateW = args.StateW;
for (int i = 1; i <= 2; ++i)
{
Thread.Sleep(5000);
Console.WriteLine("Handler{0}.Worker is running on ThreadId:" +
"{1}, i:{2}",
name, Thread.CurrentThread.ManagedThreadId, i);
}
await Task.Delay(0);
mrs.Set();
}
}
public delegate Task AsyncEventHandler<TEventArgs>(
object sender, TEventArgs e, CancellationToken token);
public static class AsynEventHandlerExtensions
{
// invoke an async event (with null-checking)
public static async Task InvokeAsync<TEventArgs>(
this AsyncEventHandler<TEventArgs> handler,
object sender, TEventArgs args, CancellationToken token)
{
await Task.Run(async () =>
{
Console.WriteLine("InvokeAsync<TEventArgs> is running on ThreadId:{0}",
Thread.CurrentThread.ManagedThreadId);
var delegates = handler?.GetInvocationList();
if (delegates?.Length > 0)
{
var tasks =
delegates
.Cast<AsyncEventHandler<TEventArgs>>()
.Select(e => Task.Run(
async () => await e.Invoke(sender, args, token)));
await Task.WhenAll(tasks);
}
}).ConfigureAwait(false);
}
}
internal class Client
{
public static async Task Main(string[] args)
{
Console.WriteLine("Client is running on ThreadId:{0}",
Thread.CurrentThread.ManagedThreadId);
ManualResetEvent[] mres = new ManualResetEvent[3];
for (int i = 0; i < mres.Length; i++)
mres[i] = new ManualResetEvent(false);
EventWrapper s = new EventWrapper();
s.EventW += (new HandlerWrapper("1", mres[0])).Handler;
s.EventW += (new HandlerWrapper("2", mres[1])).Handler;
s.EventW += (new HandlerWrapper("3", mres[2])).Handler;
// Change subject state and notify observers
s.StateW = "ABC123";
var timer = new Stopwatch();
timer.Start();
await s.Notify(CancellationToken.None);
ManualResetEvent.WaitAll(mres);
timer.Stop();
TimeSpan timeTaken = timer.Elapsed;
string tmp1 = "Client time taken: " +
timeTaken.ToString(@"m\:ss\.fff");
Console.WriteLine(tmp1);
Console.ReadLine();
}
}
这是执行结果:
从执行结果可以看出,我们看到EventHandler,现在异步在单独的线程上运行,从执行日志中可以看到并发性,总耗时为10.063秒。
使用TAP – Ver2的异步事件
虽然这不是本文的主要目的,但我们可以更改代码以更好地演示TAP模式。我们只对上面的项目代码做一个小的更改,改变一种方法,所有其他方法都与上面相同。
private async Task Worker(object subject, EventArgsW args)
{
Console.WriteLine("Handler{0}.Worker is running on ThreadId:" +
"{1}, i:0",
name, Thread.CurrentThread.ManagedThreadId);
StateW = args.StateW;
for (int i = 1; i <= 2; ++i)
{
await Task.Delay(5000);
Console.WriteLine("Handler{0}.Worker is running on ThreadId:" +
"{1}, i:{2}",
name, Thread.CurrentThread.ManagedThreadId, i);
}
mrs.Set();
}
现在,我们得到以下执行结果:
例如,如果我们把注意力集中在Handler1.Worker上,我们可以看到该异步方法已经在ID为 5,8,6的线程的三个来自ThreadPool的不同线程上运行。由于TAP模式,这一切都很好,因为await方法的工作被ThreadPool中的下一个可用线程选择。并发性再次明显,总时间为10.101秒。
结论
实际上,事件机制提供对EventHandler的同步调用。我们在上面的例子中展示了如何异步调用EventHandler。代码中提供了两种可重用的扩展方法,它们简化了异步调用实现。好处是并行调用EventHandler,这在当今的多核系统中很重要。
参考
https://www.codeproject.com/Articles/5341837/Asynchronous-Events-in-Csharp