异步编程模式

.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 类,类有同步和异步方法。

  1. 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);
        }
    }
    
  2. 演示1:同步调用。

    Person p = new Person();
    Task<string> result = p.ListenMusic();
    p.PlayGame();
    

    运行结果:同步调用,在主线程顺序输出。

  3. 演示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 关键字,等待异步的操作返回,然后再继续执行。

  4. 演示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线程之外来做。

  5. 演示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 方法。

  1. ListenMusicPlayGame 同步方法定义

    /// <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);
        }
    }
    
    
  2. 异步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;
            }
        }
    }
    
  3. 演示1:同步调用

    Boy boy = new Boy();
    boy.ListenMusic();
    boy.PlayGame();
    

    运行结果:顺序在主线程执行。

  4. 演示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);
    }
    

    运行结果如下:听歌和玩游戏同时进行,定期会收到进度的通知,听歌结束后会收到事件通知。

  5. 演示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();
    

    运行结果:用户取消听歌后,歌曲播放就结束了。

  6. 演示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 接口,这个接口只有 IsCompletedAsyncWaitHandleAsyncStateCompletedSynchronously 四个属性。IAsyncResult的对象存储异步操作的信息。

属性 说明
IsCompleted 异步操作是否完成
AsyncWaitHandle 等待异步完成的句柄(信号量)
AsyncState 用户自定义对象,可包含上下文或异步操作信息【可选的】
CompletedSynchronously 异步操作是否【同步】完成(在调用异步的线程上,而不是单独的线程池)

异步操作通过 BeginOperationName EndOperationName 两个方法实现,分别开始和结束异步操作。

  1. 开始异步操作,使用 BeginOperationName 方法

    • Begin方法具有同步版本方法 OperationName 的中的所有参数
    • Begin方法还有另一个参数 AsyncCallback 委托,在异步完成后自动调用,如不希望调用,设置成null
    • Begin方法还有另一个参数 Object 用户定义对象,一般即 AsyncState
    • Begin方法的返回值是IAsyncResult
    • Begin方法执行后,无论异步操作是否结束,都立即返回对调用线程的控制
    • 如果Begin方法引发异常,则会在异步操作之前引发异常,并且不会调用回调方法
  2. 结束异步操作,使用 EndOperationName 方法

    • End 方法用于结束异步操作 OperationName,有一个 IAsyncResult 参数,是Begin 方法的返回值
    • End 方法返回值与OperationName类型相同
    • End 方法调用时,如果 IAsyncResult 对应的异步操作没有完成,那么 End 方法将阻塞
    • 异步操作引发的异常会从 End 方法抛出。重复调用End方法,和End方法使用未返回的IAsyncResult参数的情况,应考虑引发InvalidOperationException
  3. 异步操作的阻塞,同步执行

    异步编程模型(APM)中使用阻塞实现程序同步执行有三种方式:调用EndOperationName、使用 IAsyncResult 中的 AsyncWaitHandle、使用时间轮询IsCompleted

  4. 使用委托进行异步编程

    委托有Invoke同步执行方法,和BeginInvokeEndInvoke异步方法,对同步方法使用委托就可以实现异步编程。

举例说明异步编程APM的使用方法。例子中有两个同步方法ReadBookListenMusic。同时使用委托对ReadBook同步方法封装两个异步方法BeginReadBookEndReadBook。同时还包括一个ReadBookFinishCallback回调方法。以此来演示异步编程模型(APM)中的常用的内容。

  1. 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;
    }
    
  2. BeginReadBook EndReadBook异步方法定义(使用委托封装)

    封装的 BeginReadBookEndReadBook 异步方法,就是常见的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);
    }
    
  3. ListenMusic 同步方法定义

    /// <summary>
    /// 听歌方法(用作和异步方法做对比)
    /// </summary>
    public void ListenMusic()
    {
        for (int i = 0; i < 15; i++)
        {
            Thread.Sleep(500);
            Console.WriteLine("Listening music for {0} minutes.", i);
        }
    }
    
  4. 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.");
    }
    
  5. 演示1:同步调用

    依次调用 ReadBookListenMusic 同步方法。

    Console.WriteLine("---- 同步调用 ----");
    APM amp = new APM();
    amp.ReadBook(34);
    amp.ListenMusic();
    Console.ReadKey();
    

    运行结果:同步执行,方法依次调用,读书结束后再进行听歌。

  6. 演示2:异步调用 + 异步回调

    依次调用 BeginReadBook 异步方法和 ListenMusic 同步方法,并且使用回调方法。

    Console.WriteLine("---- 异步调用 ----");
    APM amp = new APM();
    amp.BeginReadBook(34, new AsyncCallback(amp.ReadBookFinishCallback));
    amp.ListenMusic();
    Console.ReadKey();
    

    运行结果:方法依次调用,异步调用读书,调用结束后返回对调用线程(主线程)的控制。继续调用听歌的方法。读书和听歌同时进行,在听歌没有结束的时候,读书已经完成,触发ReadBookFinishCallback回调。

  7. 演示3: EndReadBook 阻塞实现同步执行

    依次调用 BeginReadBook 、BeginReadBook 异步方法和 ListenMusic方法 ,BeginReadBookListenMusic 阻塞。

    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 阻塞,ListenMusicEndReadBook 执行结束(异步执行结束)后才执行。

  8. 演示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);
    

    运行结果:这些异常需要开发者及时处理

  9. 演示5:轮询方式阻塞,实现同步执行

    Console.WriteLine("---- 异步调用-异常 ----");
    APM amp = new APM();
    var result = amp.BeginReadBook(34, null);
    while (result.IsCompleted != true)
    {
        Console.Write(".");
        Thread.Sleep(500);
    }
    Console.WriteLine("轮询结束!!!");
    

    运行结果:

  10. 演示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完成时设置的等待句柄。 可以获得 WaitHandleTask,如下所示:

WaitHandle wh = ((IAsyncResult)task).AsyncWaitHandle;

5. 示例代码

posted @ 2021-09-06 11:48  MatthewBin  阅读(84)  评论(0编辑  收藏  举报