.NET有三种异步模式编程能力。
-
基于任务的异步模式(TAP )Task-based Asynchronous Pattern
- 该模式使用单一方法表示异步操作的开始和完成,async 和 await 关键词为TAP添加了支持
- TAP 在
.NET Framework 4
中引入 - 在 .NET 中异步编程推荐的方法
-
基于事件的异步模式(EAP)Event-based Asynchronous Pattern
- 该模型是旧模型,异步行为是基于事件的
- 这种模式需要后缀为 Async 的方法、一个或多个事件、事件处理的委托类型、EventArg派生类型。
- EAP 在
.NET Framework 2.0
中引入 - 不建议再使用
?
- 异步编程模型(APM)Asynchronous Programming Model
- 该模型是旧模型,使用IAsyncResult提供异步行为,也称为 IAsyncResult 模式
- 该模式下,同步操作需要 Begin 和 End 方法
- 不建议再使用
- APM 在
.NET Framework 1.0
中引入
1 基于任务的异步模式(TAP)
TAP建议用于新开发。命名空间在System.Threading.Tasks
中。TAP的异步方法和同步方法具有相同的签名。但是有 out
和 ref
参数除外,并且应该避免它,将其作为Task<T>
的一部分返回。
TAP设计中也可以增加取消的支持,如果操作允许取消,需要增加 CancellationToken
类型参数。
TAP设计中也可以增加进度通知的支持,需要 IProgress<T>
类型参数。
在TAP中,async
和 await
关键字可以异步调用和阻塞异步方法的调用。
下面用一个示例演示TAP基本用法。例子中有 Person
类,类有同步和异步方法。
-
Listen Music
同步方法和PlayGame
同步方法。/// <summary> /// 同步听歌方法 /// </summary> public void ListenMusic(string music = "将军令") { for (int i = 0; i < 10; i++) { Console.WriteLine("I‘m listening {0} ...", music); Thread.Sleep(500); } Console.WriteLine("{0} has completed.", music); } /// <summary> /// 同步打游戏方法 /// </summary> public void PlayGame() { for (int i = 0; i < 10; i++) { Console.WriteLine("In gamming ..."); Thread.Sleep(500); } }
-
演示1:同步调用。
Person p = new Person(); Task<string> result = p.ListenMusic(); p.PlayGame();
运行结果:同步调用,在主线程顺序输出。
-
演示2:异步调用。
Person p = new Person(); p.ListenMusicAsync(); p.PlayGameAsync();
ListenMusicAsync
和PlayGameAsync
的方法定义:/// <summary> /// 异步听歌方法 /// </summary> public Task<string> ListenMusicAsync(string music = "将军令") { return Task.Run(() => { for (int i = 0; i < 10; i++) { Console.WriteLine("I‘m listening {0} time {1}...", music, i); Thread.Sleep(500); } return string.Format("music {0} finish.", music); }); } /// <summary> /// 异步打游戏 /// </summary> /// <returns></returns> public Task PlayGameAsync() { return Task.Run(() => { for (int i = 0; i < 10; i++) { Console.WriteLine("In gamming ..."); Thread.Sleep(500); } return; }); }
运行结果:
就像调用普通方法一样,方法就可以异步执行。有时候异步调用存在先后顺序。此时在调用异步操作的方法声明加上
async
关键字,调用异步方法使用await
关键字,等待异步的操作返回,然后再继续执行。 -
演示3:阻塞调用。
调用异步的外部方法添加
async
关键字,调用异步的时候使用await
关键字,同时返回值不是Task<string>
而是string
。public async static void AsyncRunBlock() { Person p = new Person(); string result = await p.ListenMusicAsync(); Console.WriteLine(result); await p.PlayGameAsync(); }
然后调用
AsyncRunBlock
方法,运行结果如下:看起来和同步调用一样,但是还是有区别的。同步方法调用听歌和玩游戏都在主线程中顺序执行。异步方法使用TAP,在执行听歌和玩游戏,其实都开启了另外的线程来执行(演示2)并不在主线程,然后我们在主线程控制了两个异步线程的前后顺序。这种技术在客户端和Web网页开发中极其有用,下载等耗时操作不应卡死界面,应该放在UI线程之外来做。
-
演示4:可取消和进度通知。
Person p = new Person(); CancellationTokenSource cts = new CancellationTokenSource(); var progress = new Progress<int>(); progress.ProgressChanged += Progress_ProgressChanged; p.ListenMusicAsync("一首凉凉送给你", cts, progress); p.PlayGameAsync(); Thread.Sleep(1500);// 1.5s后取消 cts.Cancel();
进度通知的事件:
private static void Progress_ProgressChanged(object sender, int e) { Console.WriteLine("Receive Report:{0}%", e); }
取消异步程序执行还有其他的用法,比如:
CancellationTokenSource cts = new CancellationTokenSource(2000); // 2S 后取消任务,不用显式调用Cancel方法 // 或者 cts.CancelAfter(3000); // 取消任务,并在3s后执行
运行结果:在听歌到30%的时候任务被取消,但是玩游戏的任务没有取消仍继续运行,
2 基于事件的异步模式(EAP)
一般用于执行多个任务,同时仍能响应用户交互的场景。实际上,在 System.Threading
中提供了高性能多线程的所有工具,但是有效使用它需要丰富的经验,而且所需要的工作相对较多。如果是简单的多线程应用程序,BackgroundWorker
比较适合,因为它是一种简单的多线程解决方案。对于复杂的异步应用,可以考虑使用基于事件的异步模式EAP。
EAP的目标在于让开发者像使用事件一样来编写异步的程序,并且可以支持并行执行多个操作。每个操作完成后会收到通知。EAP设计规范上还支持异步取消操作(当取消时,如果正好异步操作执行结束,就会发生“竞争条件”)。
在基于事件的异步模式(EAP)中,可设计为单调用和多调用两种方式。通过重载方法添加一个额外object
类型参数来实现。额外参数的核心目的是标识多调用情况下的实例,便于后续的的跟踪。对应的,取消异步的方法,在多调用的情况下,也要有额外的object
参数。
在基于事件的异步模式(EAP)中可以增加进度和增量的跟踪事件。多调用情况下需要识别调用的实例。
下面用一个示例演示EAP的基本用法。例子中有有一个 Boy
类,类中一个同步的 ListenMusic
方法和一个异步的 ListenMusicAsync
方法。作为对比,还有一个同步的 PlayGame
方法。
-
ListenMusic
和PlayGame
同步方法定义/// <summary> /// 同步听歌方法 /// </summary> public void ListenMusic(string music = "将军令") { for (int i = 0; i < 10; i++) { Console.WriteLine("I‘m listening {0} ...", music); Thread.Sleep(500); } Console.WriteLine("{0} has completed.", music); } /// <summary> /// 同步打游戏方法 /// </summary> public void PlayGame() { for (int i = 0; i < 10; i++) { Console.WriteLine("In gamming ..."); Thread.Sleep(500); } }
-
异步
ListenMusicAsync
方法和取消方法,外加一个测试异常的方法。方法中的代码并不十分符合面向对象规范,在此只演示用法。
Thread thread = null;
bool userCancel = false;
public event ProgressChangedEventHandler ProgressChanged;
public event ListenMusicCompletedEventHandler ListenMusicCompleted;
/// <summary>
/// 异步听歌方法
/// </summary>
public void ListenMusicAsync(string music = "将军令")
{
thread = new Thread(() =>
{
int percent = 0;
try
{
for (int i = 0; i < 10 && !userCancel; i++)
{
Console.WriteLine("I‘m listening {0} ...", music);
percent += 10;
ProgressChangedEventArgs e = new ProgressChangedEventArgs(percent, null);
ProgressChanged?.Invoke(e); // 通知进度
Thread.Sleep(500);
}
ListenMusicCompleted?.Invoke(this, new ListenMusicCompletedEventArgs(music, null, userCancel, null)); // 通知完成
}
catch (Exception ex)
{
ListenMusicCompleted?.Invoke(this, new ListenMusicCompletedEventArgs(music, ex, false, null)); // 通知完成(因取消或异常)
}
});
thread.Start();
}
/// <summary>
/// 取消异步方法
/// </summary>
public void CancelAsync()
{
userCancel = true;
}
/// <summary>
/// 测试异常
/// </summary>
public void TestAsync()
{
if (null != thread)
thread.Abort();
}
以下是完成事件的参数定义:
public class ListenMusicCompletedEventArgs : AsyncCompletedEventArgs
{
private string music = "";
private bool finish = false;
public ListenMusicCompletedEventArgs(
string music,
Exception e,
bool canceled,
object state) : base(e, canceled, state)
{
this.music = music;
}
public string Music
{
get
{
// 异步操作引发异常,推荐使用此方法,事件参数的属性将出现异常
RaiseExceptionIfNecessary();
return music;
}
}
public bool Finish
{
get
{
// 异步操作引发异常,推荐使用此方法,事件参数的属性将出现异常
RaiseExceptionIfNecessary();
return finish;
}
}
}
-
演示1:同步调用
Boy boy = new Boy(); boy.ListenMusic(); boy.PlayGame();
运行结果:顺序在主线程执行。
-
演示2:异步调用
Boy boy = new Boy(); boy.ProgressChanged += Boy_ProgressChanged; boy.ListenMusicCompleted += Boy_ListenMusicCompleted; boy.ListenMusicAsync(); boy.PlayGame();
通知回调的代码如下:
private static void Boy_ListenMusicCompleted(object sender, ListenMusicCompletedEventArgs e) { if (e.Cancelled) { Console.WriteLine("Receive Event: music has closed."); //string name = e.Music; // 此处将报错 } else { Console.WriteLine("Receive Event: music {0} is finished. ## User Cancel:{1}", e.Music, e.Cancelled); } } private static void Boy_ProgressChanged(ProgressChangedEventArgs e) { Console.WriteLine("Receive Event: music progress is {0}% ...", e.ProgressPercentage); }
运行结果如下:听歌和玩游戏同时进行,定期会收到进度的通知,听歌结束后会收到事件通知。
-
演示3:异步取消
通知回调的代码和上面一样,在开始听歌后取消。
Boy boy = new Boy(); boy.ProgressChanged += Boy_ProgressChanged; boy.ListenMusicCompleted += Boy_ListenMusicCompleted; boy.ListenMusicAsync(); Thread.Sleep(2000); Console.WriteLine("This music is boring.I‘ll shutdown it."); boy.CancelAsync();
运行结果:用户取消听歌后,歌曲播放就结束了。
-
演示4:异常
在异步线程发生异常后,通知事件中的属性不可访问。试图访问会引发异常。
Boy boy = new Boy(); boy.ProgressChanged += Boy_ProgressChanged; boy.ListenMusicCompleted += Boy_ListenMusicCompleted; boy.ListenMusicAsync(); Thread.Sleep(2000); Console.WriteLine("This music is boring.I‘ll shutdown it."); boy.CancelAsync();
值得注意的是,如果用户取消异步操作,会正常触发
ListenMusicCompleted
结束事件,回调参数中Cancelled
值是True
。此时回调参数中的属性依然不能访问,访问的话会引发上述异常。其实不难理解,用户都取消任务了,再访问属性将变的毫无意义。如果在实现EAP过程中AsyncCompletedEventArgs
属性不添加RaiseExceptionIfNecessary
方法检验,那么访问属性异常不会发生。这是不建议的。对异步程序来说,有可能会隐藏好多难以发现的问题,建议按照官方推荐方式来实现EAP。
2.1 何时使用EAP
官方描述。
一般原则,尽量使用EAP,如果无法满足一些要求,可能还需要实现 APM (IAsyncResult模式)。
何时实现 EAP 推荐指南:
- 将基于事件的模式用作公开类的异步行为的默认 API。
- 如果类主要用于客户端应用(例如,Windows 窗体),请勿公开IAsyncResult模式。
- 仅在需要满足特定要求时,才公开IAsyncResult 模式。 例如,为了与现有 API 兼容,可能需要公开IAsyncResult 模式。
- 请勿在不公开基于事件的模式的情况下公开 IAsyncResult 模式。
- 如果必须公开IAsyncResult 模式,请以高级选项的形式这样做。 例如,如果生成代理对象,默认生成的是基于事件的模式,并含用于生成IAsyncResult 模式的选项。
- 在IAsyncResult 模式实现的基础之上生成基于事件的模式实现。
- 避免对相同的类公开基于事件的模式和IAsyncResult 模式。 请对“高级”类公开基于事件的模式,并对“低级”类公开IAsyncResult 模式。 例如,比较 WebClient 组件上基于事件的模式与 HttpRequest 类上的IAsyncResult 模式。
- 出于兼容性需要,可以对相同的类公开基于事件的模式和IAsyncResult 模式。 例如,如果已释放使用IAsyncResult 模式的 API,需要保留IAsyncResult 模式,以实现向后兼容性。
- 如果生成的对象模型复杂性远远超过分离实现的好处,请对相同的类公开基于事件的模式和IAsyncResult 模式。 对一个类公开两种模式优于避免公开基于事件的模式。
- 如果必须对一个类公开基于事件的模式和IAsyncResult 模式,请将EditorBrowsableAttribute设置为 Advanced,以将IAsyncResult 模式实现标记为高级功能。 这会指示设计环境(如 Visual Studio IntelliSense)不显示IAsyncResult 属性和方法。 这些属性和方法仍完全可用,这样做只是为了让使用 IntelliSense 的开发人员对 API 更加明确。
何时公开 IAsyncResult 模式的条件:
IAsyncResult 模式比基于事件的模式更适用 的情况有三种:
- 对 IAsyncResult 阻止等待操作
- 对多个 IAsyncResult 对象阻止等待操作
- 对 IAsyncResult 轮询完成状态
虽然可以使用基于事件的模式来处理这些情况,但这样做比使用 IAsyncResult 模式更不方便。
开发人员经常对性能要求通常很高的服务使用 IAsyncResult 模式。 例如,轮询完成状态就是一种高性能服务器技术。
此外,基于事件的模式的效率低于 IAsyncResult 模式,因为前者创建的对象更多(尤其是EventArgs),并且跨线程同步。
下面列出了一些在决定使用 IAsyncResult 模式时要遵循的建议:
- 仅在特别需要对 WaitHandle 或IAsyncResult 对象的支持时,才公开 IAsyncResult 模式。
- 仅在有使用 IAsyncResult 模式的现有 API 时,才公开 IAsyncResult 模式。
- 如果有基于 IAsyncResult 模式的现有 API,还请考虑在下一个版本中公开基于事件的模式。
- 仅在有高性能要求,且已验证无法通过基于事件的模式满足这些要求,但可以通过 IAsyncResult 模式满足时,才公开 IAsyncResult 模式。
3 异步编程模型(APM)
异步编程模型的核心是 IAsyncResult
接口,这个接口只有 IsCompleted
、AsyncWaitHandle
、AsyncState
、CompletedSynchronously
四个属性。IAsyncResult
的对象存储异步操作的信息。
属性 | 说明 |
---|---|
IsCompleted | 异步操作是否完成 |
AsyncWaitHandle | 等待异步完成的句柄(信号量) |
AsyncState | 用户自定义对象,可包含上下文或异步操作信息【可选的】 |
CompletedSynchronously | 异步操作是否【同步】完成(在调用异步的线程上,而不是单独的线程池) |
异步操作通过 BeginOperationName
和 EndOperationName
两个方法实现,分别开始和结束异步操作。
-
开始异步操作,使用
BeginOperationName
方法- Begin方法具有同步版本方法
OperationName
的中的所有参数 - Begin方法还有另一个参数
AsyncCallback
委托,在异步完成后自动调用,如不希望调用,设置成null
- Begin方法还有另一个参数
Object
用户定义对象,一般即AsyncState
- Begin方法的返回值是
IAsyncResult
- Begin方法执行后,无论异步操作是否结束,都立即返回对调用线程的控制
- 如果Begin方法引发异常,则会在异步操作之前引发异常,并且不会调用回调方法
- Begin方法具有同步版本方法
-
结束异步操作,使用
EndOperationName
方法- End 方法用于结束异步操作
OperationName
,有一个IAsyncResult
参数,是Begin 方法的返回值 - End 方法返回值与
OperationName
类型相同 - End 方法调用时,如果
IAsyncResult
对应的异步操作没有完成,那么 End 方法将阻塞 - 异步操作引发的异常会从 End 方法抛出。重复调用End方法,和End方法使用未返回的
IAsyncResult
参数的情况,应考虑引发InvalidOperationException
。
- End 方法用于结束异步操作
-
异步操作的阻塞,同步执行
异步编程模型(APM)中使用阻塞实现程序同步执行有三种方式:调用
EndOperationName
、使用IAsyncResult
中的AsyncWaitHandle
、使用时间轮询IsCompleted
。 -
使用委托进行异步编程
委托有
Invoke
同步执行方法,和BeginInvoke
、EndInvoke
异步方法,对同步方法使用委托就可以实现异步编程。
举例说明异步编程APM的使用方法。例子中有两个同步方法ReadBook
、ListenMusic
。同时使用委托对ReadBook同步方法封装两个异步方法BeginReadBook
和EndReadBook
。同时还包括一个ReadBookFinishCallback
回调方法。以此来演示异步编程模型(APM)中的常用的内容。
-
ReadBook同步方法定义
/// <summary> /// 同步读书方法 /// </summary> /// <returns></returns> public int ReadBook(int planPage) { Console.WriteLine("Begin read book..."); Thread.Sleep(5000); Console.WriteLine("End read book.Total {0} pages.", planPage); return planPage; }
-
BeginReadBook
和EndReadBook
异步方法定义(使用委托封装)封装的
BeginReadBook
和EndReadBook
异步方法,就是常见的APM异步方法。一般使用此种方式实现异步的框架或者库都是以这种形式提供。/// <summary> /// 同步读书方法(用来自己实现一个Begin方法) /// </summary> public delegate int ReadBookDelegate(int page); /// <summary> /// 异步读书开始方法 /// </summary> public IAsyncResult BeginReadBook(int planPage, AsyncCallback callback) { ReadBookDelegate call = ReadBook; return call.BeginInvoke(planPage, callback, call); } /// <summary> /// 异步读书结束方法 /// </summary> public int EndReadBook(IAsyncResult ar) { ar.AsyncWaitHandle.WaitOne(); var call = (ReadBookDelegate)ar.AsyncState; return call.EndInvoke(ar); }
-
ListenMusic
同步方法定义/// <summary> /// 听歌方法(用作和异步方法做对比) /// </summary> public void ListenMusic() { for (int i = 0; i < 15; i++) { Thread.Sleep(500); Console.WriteLine("Listening music for {0} minutes.", i); } }
-
ReadBookFinishCallback
回调函数定义public void ReadBookFinishCallback(IAsyncResult result) { // Get the state object associated with this request. ReadBookDelegate call = (ReadBookDelegate)result.AsyncState; Console.WriteLine("ReadBookFinishCallback and then go to park."); }
-
演示1:同步调用
依次调用
ReadBook
和ListenMusic
同步方法。Console.WriteLine("---- 同步调用 ----"); APM amp = new APM(); amp.ReadBook(34); amp.ListenMusic(); Console.ReadKey();
运行结果:同步执行,方法依次调用,读书结束后再进行听歌。
-
演示2:异步调用 + 异步回调
依次调用
BeginReadBook
异步方法和ListenMusic
同步方法,并且使用回调方法。Console.WriteLine("---- 异步调用 ----"); APM amp = new APM(); amp.BeginReadBook(34, new AsyncCallback(amp.ReadBookFinishCallback)); amp.ListenMusic(); Console.ReadKey();
运行结果:方法依次调用,异步调用读书,调用结束后返回对调用线程(主线程)的控制。继续调用听歌的方法。读书和听歌同时进行,在听歌没有结束的时候,读书已经完成,触发
ReadBookFinishCallback
回调。 -
演示3:
EndReadBook
阻塞实现同步执行依次调用
BeginReadBoo
k 、BeginReadBook
异步方法和ListenMusic
方法 ,BeginReadBook
对ListenMusic
阻塞。Console.WriteLine("---- 异步调用-阻塞 ----"); APM amp = new APM(); var result = amp.BeginReadBook(34, null); int pages = amp.EndReadBook(result); amp.ListenMusic(); Console.WriteLine("EndReadBook返回值同ReadBook:{0}", pages);
运行结果:在调用
BeginReadBook
异步调用后,EndReadBook
阻塞,ListenMusic
在EndReadBook
执行结束(异步执行结束)后才执行。 -
演示4:对
EndReadBook
重复调用会出现异常Console.WriteLine("---- 异步调用-异常 ----"); APM amp = new APM(); var result = amp.BeginReadBook(34, null); int pages = amp.EndReadBook(result); pages = amp.EndReadBook(result); amp.ListenMusic(); Console.WriteLine("EndReadBook返回值同ReadBook:{0}", pages);
运行结果:这些异常需要开发者及时处理
-
演示5:轮询方式阻塞,实现同步执行
Console.WriteLine("---- 异步调用-异常 ----"); APM amp = new APM(); var result = amp.BeginReadBook(34, null); while (result.IsCompleted != true) { Console.Write("."); Thread.Sleep(500); } Console.WriteLine("轮询结束!!!");
运行结果:
-
演示6:
WaitOne
阻塞,实现同步执行
Console.WriteLine("---- 异步调用-WaitOne阻塞 ----");
APM amp = new APM();
var result = amp.BeginReadBook(34, null);
result.AsyncWaitHandle.WaitOne();
amp.ListenMusic();
运行结果:
4. 不同异步模式之间互操作
基于任务的异步模式(TAP)虽然是新编程所推荐的,但也不是万能的。有些场景使用基于事件的异步模式(EAP)比较合适。异步编程模型(APM)用起来不太友好,但是EAP的性能要比APM差,对于性能要求高的服务,用APM要比EAP合适的太多。
异步编程模式中的三种方法都有其存在的合理性。在白嫖别人的库的时候,经常遇到不同的异步操作方式。所以互操作就显得很重要,我们可以将APM和EAP迁移到TAP,也可以把TAP迁移成APM和EAP来达到兼容性。
4.1 APM -> TAP
以 Read 方法为例,其 ATP 的实现如下:
// 同步方法
public int Read(byte[] buffer, int offset, int count);
// APM 异步开始方法
public IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state);
// APM 异步结束方法
public int EndRead(IAsyncResult asyncResult);
我们使用 TaskFactory<T>.FromAsync
方法来实现 TAP 包装:
public static Task<int> ReadAsync(this Stream stream, byte[] buffer, int offset, int count)
{
if (stream == null)
throw new ArgumentNullException("stream");
return Task<int>.Factory.FromAsync(stream.BeginRead, stream.EndRead, buffer, offset, count, null);
}
这种实现类似以下内容:
public static Task<int> ReadAsync(this Stream stream, byte [] buffer, int offset, int count)
{
if (stream == null)
throw new ArgumentNullException("stream");
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, iar =>
{
try {
tcs.TrySetResult(stream.EndRead(iar));
}
catch(OperationCanceledException) {
tcs.TrySetCanceled();
}
catch(Exception exc) {
tcs.TrySetException(exc);
}
}, null);
return tcs.Task;
}
4.2 TAP -> APM
如果现有的基础结构需要 APM 模式,则还需要采用 TAP 实现并在需要 APM 实现的地方使用它。 由于任务可以组合,并且 Task
类实现 IAsyncResult
,您可以使用一个简单的 helper 函数执行此操作。 以下代码使用 Task
类的扩展,但可以对非泛型任务使用几乎相同的函数。
public static IAsyncResult AsApm<T>(this Task<T> task, AsyncCallback callback, object state)
{
if (task == null)
throw new ArgumentNullException("task");
var tcs = new TaskCompletionSource<T>(state);
task.ContinueWith(t =>
{
if (t.IsFaulted)
tcs.TrySetException(t.Exception.InnerExceptions);
else if (t.IsCanceled)
tcs.TrySetCanceled();
else
tcs.TrySetResult(t.Result);
if (callback != null)
callback(tcs.Task);
}, TaskScheduler.Default);
return tcs.Task;
}
现在,请考虑具有以下 TAP 实现的用例:
public static Task<String> DownloadStringAsync(Uri url)
并且想要提供此 APM 实现:
public IAsyncResult BeginDownloadString(Uri url, AsyncCallback callback, object state);
public string EndDownloadString(IAsyncResult asyncResult);
以下示例演示了一种向 APM 迁移的方法:
public IAsyncResult BeginDownloadString(Uri url, AsyncCallback callback, object state)
{
return DownloadStringAsync(url).AsApm(callback, state);
}
public string EndDownloadString(IAsyncResult asyncResult)
{
return ((Task<string>)asyncResult).Result;
}
4.3 EAP -> TAP
包装EAP比包装 APM 模式更为复杂,因为与 APM 模式相比,EAP 模式的变体更多,结构更少。 为了演示,以下代码包装了 DownloadStringAsync
方法。 DownloadStringAsync
接受 URI,在下载时引发 DownloadProgressChanged
事件,以报告进度的多个统计信息,并在完成时引发 DownloadStringCompleted
事件。 最终在指定 URI 中返回一个字符串,其中包含页面内容。
public static Task<string> DownloadStringAsync(Uri url)
{
var tcs = new TaskCompletionSource<string>();
var wc = new WebClient();
wc.DownloadStringCompleted += (s,e) =>
{
if (e.Error != null)
tcs.TrySetException(e.Error);
else if (e.Cancelled)
tcs.TrySetCanceled();
else
tcs.TrySetResult(e.Result);
};
wc.DownloadStringAsync(url);
return tcs.Task;
}
4.4 等待句柄和TAP
4.4.1 等待句柄 -> TAP
虽然等待句柄不能实现异步模式,但高级开发人员可以在设置等待句柄时使用 WaitHandle
类和 ThreadPool.RegisterWaitForSingleObject
方法实现异步通知。 可以包装RegisterWaitForSingleObject
方法以在等待句柄中启用针对任何同步等待的基于任务的替代方法:
public static Task WaitOneAsync(this WaitHandle waitHandle)
{
if (waitHandle == null)
throw new ArgumentNullException("waitHandle");
var tcs = new TaskCompletionSource<bool>();
var rwh = ThreadPool.RegisterWaitForSingleObject(waitHandle, delegate { tcs.TrySetResult(true); }, null, -1, true);
var t = tcs.Task;
t.ContinueWith( (antecedent) => rwh.Unregister(null));
return t;
}
使用此方法,可以在异步方法中使用现有 WaitHandle
实现。 例如,若要限制在任何特定时间执行的异步操作数,可以利用信号灯(System.Threading.SemaphoreSlim
) 对象)。 可以将并发运行的操作数目限制到 N,方法为:初始化到 N 的信号量的数目、在想要执行操作时等待信号量,并在完成操作时释放信号量 :
static int N = 3;
static SemaphoreSlim m_throttle = new SemaphoreSlim(N, N);
static async Task DoOperation()
{
await m_throttle.WaitAsync();
// do work
m_throttle.Release();
}
4.4.2 TAP -> 等待句柄
正如前面所述,Task
类实现IAsyncResult
,且该实现公开IAsyncResult.AsyncWaitHandle
属性,该属性会返回在Task
完成时设置的等待句柄。 可以获得 WaitHandle
的Task
,如下所示:
WaitHandle wh = ((IAsyncResult)task).AsyncWaitHandle;