注:这是大概四年前写的文章了。并且我离开.net领域也有四年多了。原本不想再发表,可是这其实是Active Object模式在.net中的一种重要实现方法,所以我把它掏出来发布一下。若是该模型有新的发展,望在评论中帮给出一个引用,以便其它读者知晓。感激涕零html
基于事件的异步模型其实是MSDN中讲解异步编程时所提供的一个章节。但在阅读这些章节时,我以为MSDN在一开始就将全部组成所有列出,而后再联系到一块儿的讲解次序并不适合咱们的思惟方式。所以在本文中,我将按照从易到难的方式逐步对该异步模型进行讲解。编程
另外,咱们对模型的讨论将大量使用两个名词:用户代码及非用户代码。在本文中,它们分别表明使用该模型所暴露接口的代码以及模型的内部实现。多线程
模型简介异步
您首先须要明白的一点是,该异步模型并非在为您提供一种新的多线程编程技术,而是为您提供了一些多线程编程规范方面的建议,从而为您的多线程解决方案提供更良好更灵活的接口和内部组织。就其实现而言,基于事件的异步模型其实是对其内部所包含的异步操做的一个包装。而按照该规范组织的异步调用对于用户代码而言则意义很是明确:若是须要以异步方式调用一个操做,并在操做完成后接到相应的通知,而不须要和工做项进行进一步的通信,那么能够直接调用一个函数并侦听一个事件便可。甚至咱们更能够经过侦听报告工做进度的事件来获得工做项进度更新的信息。async
首先咱们来看看异步模型的使用。假设咱们有一个异步模型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。您能够看到,这些成员的名称经常与您所须要插入的工做项的名称相关。函数
同时也能够看出,在基于事件的异步模型中,对多线程部分的处理被所有置于模型内部,而仅仅暴露按照同步调用方式使用所须要的各个接口。也就是说,对于该模型的用户代码而言,其仅仅须要按照普通的单线程处理逻辑调用该模型所提供的接口便可,而不须要考虑有关多线程的任何问题。this
模型接口spa
在大概了解了基于事件的异步模型以后,让咱们先从模型的表观特征,模型的接口开始提及。通过这一节,相信您能了解该异步模型所提供的各个成员的确切使用方式。.net
首先咱们来说讲异步操做的启动函数。对于一个须要转化为异步操做的同步方法,咱们经常须要按照必定的步骤将其转化为异步调用。
第一步要转化的是函数名称。在同步方法的转化过程当中,咱们经常须要对函数的名称进行更改。更改后的函数名称一方面能够提供较为明显的“这是使用异步模型的成员函数”的提示,更能够令其与同步函数同时存在。通常状况下,异步函数会在相应的同步函数名以后添加-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
公众号必定帮忙别标成原创,由于协调起来太麻烦了。。。