.NET - 基于事件的异步模型

时间:2022-05-18 22:33:50

注:这是大概四年前写的文章了。而且我离开.net领域也有四年多了。本来不想再发表,但是这实际上是Active Object模式在.net中的一种重要实现方法,因此我把它掏出来发布一下。如果该模型有新的发展,望在评论中帮给出一个引用,以便其它读者知晓。感激不尽

  基于事件的异步模型实际上是MSDN中讲解异步编程时所提供的一个章节。但在阅读这些章节时,我觉得MSDN在一开始就将所有组成全部列出,然后再联系到一起的讲解次序并不适合我们的思维方式。因此在本文中,我将按照从易到难的方式逐步对该异步模型进行讲解。

  另外,我们对模型的讨论将大量使用两个名词:用户代码及非用户代码。在本文中,它们分别代表使用该模型所暴露接口的代码以及模型的内部实现。

 

模型简介

  您首先需要明白的一点是,该异步模型并不是在为您提供一种新的多线程编程技术,而是为您提供了一些多线程编程规范方面的建议,从而为您的多线程解决方案提供更良好更灵活的接口和内部组织。就其实现而言,基于事件的异步模型实际上是对其内部所包含的异步操作的一个包装。而按照该规范组织的异步调用对于用户代码而言则意义非常明确:如果需要以异步方式调用一个操作,并在操作完成后接到相应的通知,而不需要和工作项进行进一步的通讯,那么可以直接调用一个函数并侦听一个事件即可。甚至我们更可以通过侦听报告工作进度的事件来得到工作项进度更新的信息。

  首先我们来看看异步模型的使用。假设我们有一个异步模型EventAsyncModel,而其拥有一个可以被异步执行的任务SomeTask:

 1 void StartTask(EventAsyncModel asyncModel)
2 {
3 asyncModel.SomeTaskCompleted += OnSomeTaskCompleted;
4 asyncModel.SomeTaskProgressChanged += OnSomeTaskProgressChanged;
5
6 asyncModel.SomeTaskAsync();
7 }
8
9 void OnSomeTaskProgressChanged(…)
10 {
11
12 }
13
14 void OnSomeTaskCompleted(…)
15 {
16
17 }

  上面的代码展示了用户代码如何对基于事件的异步模型进行使用:用户通过一个成员函数SomeTaskAsync()插入一个工作项,并在工作项状态发生变化时发出一系列事件,如任务的进度变化的SomeTaskProgressChanged或完成的SomeTaskCompleted。您可以看到,这些成员的名称常常与您所需要插入的工作项的名称相关。

 

  同时也可以看出,在基于事件的异步模型中,对多线程部分的处理被全部置于模型内部,而仅仅暴露按照同步调用方式使用所需要的各个接口。也就是说,对于该模型的用户代码而言,其仅仅需要按照普通的单线程处理逻辑调用该模型所提供的接口即可,而不需要考虑有关多线程的任何问题。

 

模型接口

  在大概了解了基于事件的异步模型之后,让我们先从模型的表观特征,模型的接口开始说起。经过这一节,相信您能了解该异步模型所提供的各个成员的确切使用方式。

  首先我们来讲讲异步操作的启动函数。对于一个需要转化为异步操作的同步方法,我们常常需要按照一定的步骤将其转化为异步调用。

  第一步要转化的是函数名称。在同步方法的转化过程中,我们常常需要对函数的名称进行更改。更改后的函数名称一方面可以提供较为明显的“这是使用异步模型的成员函数”的提示,更可以令其与同步函数同时存在。一般情况下,异步函数会在相应的同步函数名之后添加-Async后缀。例如对于同步函数SomeTask(),我们需要提供异步函数SomeTaskAsync()。

  在确定了异步函数的名称之后,我们要转化的就是函数的参数。一个异步函数所使用的参数应与相应同步函数所使用的参数相同。例如对于同步函数SomeTask(string parameters),异步函数的签名应为SomeTaskAsync(string parameters)。

  该过程中较为例外的是out和ref参数。如果同步调用中包含一个out参数,那么它将存在于表示返回值的组成中,即异步模型的Complete事件中。而如果同步调用中包含一个ref参数,那么它需要同时存在于异步函数的参数列表以及表示返回值的组成中。例如对于同步函数SomeTask(string parameters, out int result),转化后的异步调用则为SomeTaskAsync(string parameters),同时Complete事件所传回的参数将带有参数result。同样地,对于同步函数SomeTask(string parameters, ref int result),转化后的异步调用应为SomeTaskAsync(string parameters, int result),同时Complete事件所传回的参数将带有参数result。

  最后要说的是返回值。异步调用的返回值一般为void,这是因为它在函数调用返回时还没有得到工作项的最终执行结果。

  既然异步调用并不将执行结果通过返回值传递,那返回值应从哪里得到呢?答案是工作项的完成消息。对于一个需要返回执行结果的异步操作,软件开发人员常常需要从AsyncCompletedEventArgs派生,并将表示执行结果的成员添加到该派生类中。在异步工作项执行完毕以后,AsyncCompletedEventArgs类的派生类将被Completed事件返回,并在派生类中记录工作项的执行结果。例如对于一个返回类型为int的同步函数int SomeTask(),我们首先需要创建AsyncCompletedEventArgs的派生类:

 1 public class SomeTaskCompletedArgs : AsyncCompletedEventArgs
2 {
3 public SomeTaskCompletedArgs(int result, Exception error, bool cancelled, object userState)
4 : base(error, cancelled, userState)
5 {
6 _result = result;
7 }
8
9 public int result
10 {
11 get { return _result; }
12 }
13
14 int _result;
15 }

                另外,基于事件的异步模式常常提供了汇报进度的事件。该事件的名称常常由异步模式的实现类是否具有多个异步操作来决定。对于一个具有多个异步操作的实现类来说,函数SomeTaskAsync()所对应的事件名应为SomeTaskProgressChanged;而对于只有一个异步操作的实现类,该事件的名称应为ProgressChanged。这些事件所返回的ProgressChangedEventArgs带有一个属性ProgressPercentage,以允许用户代码根据当前的进度更新滚动条等UI组成。

 

异步模型的多调用

  异步模型中的另一个非常重要的概念就是多调用:异步操作的实现一般来说分为两种方式:单调用和多调用。单调用在当前工作项没有完成时不允许再次执行,而多调用则允许多个工作项同时在后台执行。用户代码常常需要通过方法的签名区分这两种方式的函数异步操作:多调用接口常常提供了一个名为userState的额外参数。我们将在后面的章节中详细介绍该参数。

  接下来要转化的是函数的参数。异步函数的参数将根据其是否支持多调用而略有不同。如果一个异步函数仅仅是一个单调用函数,那么该函数所使用的参数应与相应同步函数所使用的参数相同。如果异步函数支持多调用,那么该函数需要在相应同步函数所使用的参数之后添加一个object类型的userState实例作为参数。例如对于同步函数SomeTask(string parameters),不支持多调用的异步函数应为SomeTaskAsync(string parameters),而支持多调用的异步函数应为SomeTaskAsync(string parameters, object userState)。

  该过程中较为例外的是out和ref参数。如果同步调用中包含一个out参数,那么它将存在于表示返回值的组成中,如Complete事件中。而如果同步调用中包含一个ref参数,那么它需要同时存在于异步函数的参数列表以及表示返回值的组成中。例如对于同步函数SomeTask(string parameters, out int result),转化后的仅支持单调用的异步调用则为SomeTaskAsync(string parameters),同时Complete事件所传回的参数将带有参数result。同样地,对于同步函数SomeTask(string parameters, ref int result),转化后的支持单调用的异步调用应为SomeTaskAsync(string parameters, int result),同时Complete事件所传回的参数将带有参数result。

  在介绍多调用和单调用时,我们提到了参数userState。实际上,对参数userState的使用贯穿了整个基于事件的异步模型的实现中。从插入工作项到工作项完成,工作项取消以及工作项进度更新,该参数都会附加在相应的函数调用或事件参数中,从而允许模型的用户代码了解到底是哪个工作项发生了变化,即要求userState在整个异步模型的使用过程中是唯一的,能唯一标明工作项。

  也正是由于这种唯一性要求,传入多调用接口的userState对象需要用户代码自行生成。一般情况下,用户需要自行提供对该参数的管理,如保证userState对象不会重复等等。而在模型的内部实现中,您常常需要将userState对象添加到一个集合中。而在执行完毕后,您需要从该集合中检索该对象,执行相应的处理逻辑,并最终将其从集合中删除。

  基于事件的异步模型常常需要执行对异步函数所插入的工作项的取消,而执行取消功能的函数则需要根据异步调用是否支持多调用以及表示异步模型的类型中是否仅有一个支持取消的异步操作。对于异步操作SomeTaskAsync(),取消工作项的各函数名将如下表所示:

 

支持多调用

不支持多调用

类只包含一个异步操作

void SomeTaskAsyncCancel(object userState)

void SomeTaskAsyncCancel()

类包含多个异步操作

void AsyncCancel(object userState)

void AsyncCancel()

  对于一个模型中所包含的多个异步函数,您可以根据上表所列出的转化方式依次执行转化。最终的转化结果可能包含上表列出的两个函数。例如对于模型中的支持多调用的函数TaskAsyncA()和不支持多调用的函数TaskAsyncB(),模型中将同时出现AsyncCancel(object userState)及AsyncCancel()两个函数。

  从上面的列表中也可以看出,在类型包含多个异步操作并且支持多调用的函数的情况下,AsyncCancel()所传入的userState参数应能标示出所有异步函数插入的工作项,而不仅仅区分单个异步方法所产生的工作项。

 

模型实现

  难道仅仅通过名称就能得到异步执行的功能?并不是这样。为各组成指明命名规则仅仅会使代码具有更为明显的特征,使代码具有更为明显的特征,代码更容易理解,减少出错可能,从而降低维护开销。而实际的多线程功能则由其所实际包含的逻辑中。您可以使用您所熟悉的任何多线程编程方法。但是在本文中,我们将会向您介绍一个您可能并不熟悉的方法:使用AsyncManager。

  该类型提供了一个静态成员函数CreateOperation()。其用来创建AsyncOperation类型的实例。拥有这样一个特点:通过调用该AsyncOperation类实例的Post()及PostOperationCompleted()函数,您可以将委托调用转发至创建AsyncOperation类型实例的线程中。也就是说,如果AsyncOperation类型实例是在A线程中创建的,却被传递到B线程中,那么B线程中的执行逻辑就可以通过 Post()以及PostOperationComplete()函数向A线程发送消息,以在A线程中执行特定逻辑。

  现在我们就来看看该如何通过AsyncOperation来实现基于事件的异步模型,我们首先需要调用AsyncOperationManager的CreateOperation()函数,并将该函数创建的AsyncOperation实例作为参数传入异步调用中:

1 private void BeginDownloadAsync(string link)
2 {
3 AsyncOperation operation = AsyncOperationManager.CreateOperation(link);
4 RssDownloadWorkerHandler worker = new RssDownloadWorkerHandler(DownloadRss);
5 worker.BeginInvoke(link, operation, null, null);
6 }

  在上面的代码中,link是需要下载的RSS的所在地址,而DownloadRss则是真正执行执行逻辑的函数。我们通过调用委托的BeginInvoke()函数在线程池中启动对该函数的执行。而在委托所包装的函数中,传入异步调用的AsyncOperation实例将作为函数的参数:

1 private void DownloadRss(string link, AsyncOperation operation)…

  在这里,由于我们允许同时下载多个RSS源,因此使用多调用的异步模型是较为合适的。同时,重复下载同一个地址所指向的RSS源是没有必要的,因此BeginDownloadAsync()函数所传入的参数link则相当于userState参数,用以区别各工作项。作为一个实现标准,如果当前下载项中已经拥有了BeginDownloadAsync()函数所标示的RSS地址,那么异步模型需要抛出一个表示下载项重复的ArgumentException类型的异常。这里需要注意的是,异常是线程相关的。为了能让用户代码探测到该异常,我们应在主线程中抛出异常。这也就迫使我们在主线程中管理当前的工作项ID并执行对多调用的检查。

  反过来,如果异步模型需要将RSS的下载实现为单调用,那么在主线程中所需要执行的检查就需要是当前是否拥有工作项没有完成,并在当前具有工作项的情况下抛出一个InvalidOperationException异常。同时我们还需要为模型添加IsBusy属性,以用来提示用户代码是否可以插入下一个工作项,从而避免多次插入工作项所导致的异常。

  接下来要讨论的是如何实现异步模型的执行逻辑。在编写执行逻辑的过程中,如果您希望触发特定事件,那么您需要通过AsyncOperation的Post()或PostOperationCompleted()函数向原线程插入一个委托。这两个函数都接受两个参数:第一个表示需要在AsyncOperation实例的创建线程上执行的函数,而第二个参数则表示需要传递给该函数的参数。就以DownloadRss()函数为例:

 1 private void DownloadRss(string link, AsyncOperation operation)
2 {
3 ……
4 // 对OnProgressChangedInternal的执行将从后台线程转至operation的创建线程中
5 ProgressChangedEventArgs progressArgs = new ProgressChangedEventArgs(percentage);
6 operation.Post(OnProgressChangedInternal, args);
7
8 ……
9 TaskCompleteEventArgs args = new TaskCompleteEventArgs(link, succeeded, source);
10 operation.PostOperationCompleted(OnTaskCompleteInternal, args);
11 }

  而在Post()和PostOperationComplete()函数所传入的执行函数则会运行在创建AsyncOperation实例的线程中,儿不是当前线程。因此在该传入的函数中,您应真正地发出事件。就以OnTaskCompleteInternal()为例:

1 // 该函数在创建AsyncOperation实例的线程中执行
2 private void OnTaskCompleteInternal(object param)
3 {
4 TaskCompleteEventArgs args = param as TaskCompleteEventArgs;
5
6 mTasks.Remove(args.Link);
7 if (TaskComplete != null)
8 TaskComplete(this, args); // 真正地引发事件
9 }

  另一个需要考虑的操作就是工作项的取消。首先,您需要将工作项编写为可取消的形式。在工作项执行过程中,您需要一种方式,如标志位等,通知工作项当前任务应当取消,并在工作项自身执行过程中对该标志位进行探测。同时,在成功地取消了工作项的执行之后,您需要发送相应的Completed事件,并将其成员属性Canceled设置为true,以区别真正的工作项完成所发出的Completed事件。

  同时您还需要理解争用条件这一名词。在异步模型执行过程中,下载可能恰好在发送了工作项取消这一请求后完成了。在这种情况下,我们会认为其成功完成,从而不再将AsyncCompletedEventArgs的Cancelled属性设置为true。

 

AsyncOperation内部实现

  总的来说,对AsyncOperation的使用就是通过AsyncOperation实例保持对主线程的引用,并在需要从后台线程向主线程中发送消息时向AsyncOpertation实例所记录的主线程注册回调逻辑。这种通过记录创建线程来进行线程管理的方法是在.net开发中非常常用的,也非常值得我们借鉴。经过适当简化后的相关代码如下所示:

 1 public static class AsyncOperationManager
2 {
3 public static AsyncOperation CreateOperation(object userSuppliedState)
4 {
5 return AsyncOperation.CreateOperation(userSuppliedState,
6 SynchronizationContext.Current);
7 }
8 ……
9 }
10
11 public sealed class AsyncOperation
12 {
13 private AsyncOperation(object userSuppliedState, SynchronizationContext syncContext)
14 {
15 this.syncContext = syncContext;
16 }
17
18 public void Post(SendOrPostCallback d, object arg)
19 {
20 this.syncContext.Post(d, arg);
21 }
22 ……
23 }

 

总结

  最后来一点总结。在不了解基于事件的异步模型的众多组成之前,我们并无法清晰地体会到其所具有的优点。首先,基于事件的异步模型提供的是大家所最熟悉的事件/委托模型以及成员函数,从而对用户代码而言是最自然也最容易接受的。另外,事件/委托模型可以将对多线程内容的处理隐藏到模型的内部,对多线程的处理局限于模型内部,大大增强了代码的可维护性。

 

转载请注明原文地址并标明转载:http://www.cnblogs.com/loveis715/p/5250217.html

商业转载请事先与我联系:silverfox715@sina.com

公众号一定帮忙别标成原创,因为协调起来太麻烦了。。。