[翻譯]擴展C#中的異步方法


翻譯自一篇博文,原文:Extending the async methods in C#

異步系列

  • 剖析C#中的異步方法
  • 擴展C#中的異步方法
  • C#中異步方法的性能特點。
  • 用一個用戶場景來掌握它們

在上一篇中我們討論了C#編譯器是如何轉換異步方法的。在這一篇,我們將重點討論C#編譯器為自定義異步方法的行為提供的可擴展性。

關於如何控制異步方法機制有3種方法:

  • System.Runtime.CompilerServices命名空間中提供你自己的async method builder。
  • 使用自定義的task awaiter。
  • 定義你自己的“類任務”(task-like)類型

System.Runtime.CompilerServices 命名空間中的自定義類型

上一篇文章中我們已經知道,異步方法被C#編譯器轉換從而生成的狀態機是依靠於某些預定義的類型的。但是C#編譯器卻並不一定要求這些眾所周知的類型來自於某個特定的程序集。例如,你可以在你的項目中提供自己對AsyncVoidMethodBuilder的實現,然后C#編譯器就會把異步機制“綁定”到你的自定義類型。

這種方式可以很好地讓我們來探索底層的轉換,以及運行時發生了什么:

namespace System.Runtime.CompilerServices
{
    // 你自己項目中的AsyncVoidMethodBuilder.cs
    public class AsyncVoidMethodBuilder
    {
        public AsyncVoidMethodBuilder()
            => Console.WriteLine(".ctor");
 
        public static AsyncVoidMethodBuilder Create()
            => new AsyncVoidMethodBuilder();
 
        public void SetResult() => Console.WriteLine("SetResult");
 
        public void Start<TStateMachine>(ref TStateMachine stateMachine)
            where TStateMachine : IAsyncStateMachine
        {
            Console.WriteLine("Start");
            stateMachine.MoveNext();
        }
 
        // AwaitOnCompleted, AwaitUnsafeOnCompleted, SetException 
        // 和SetStateMachine都不提供具體實現
    }   
}

現在,在你的項目中的每一個異步方法都會使用這個自定義的AsyncVoidMethodBuilder。我們可以用一個簡單的異步方法來測試它:

[Test]
public void RunAsyncVoid()
{
    Console.WriteLine("Before VoidAsync");
    VoidAsync();
    Console.WriteLine("After VoidAsync");
 
    async void VoidAsync() { }
}

測試的輸出如下:

Before VoidAsync
.ctor
Start
SetResult
After VoidAsync

你也可以實現UnsafeAwaitOnComplete方法來測試帶有await字句,也就是返回一個未完成的任務的異步方法的行為。完整的例子可以在這里找到:github.

若想改變async Task方法和async Task<T>方法的行為,則需要提供自定義的AsyncTaskMethodBuilderAsyncTaskMethodBuilder的實現。

這些類型的完整示例可以在我的名為EduAsync的github項目中的AsyncTaskBuilder.csAsyncTaskMethodBuilderOfT.cs中分別找到。

感謝Jon Skeet為這個項目帶來的靈感。這真的是一個很好的方法來更加深入地學習異步機制。

自定義的awaiter

前面的例子並不優雅,顯然不適合用於生產環境。我們可以通過這種方式學習異步機制,但肯定不希望在自己的代碼庫中看到這樣的代碼。C#的作者們已在編譯器中內置了適當的可擴展點,從而允許在異步方法中“等待(await)”不同的類型。

為了使一個類型是“可等待的”(即在await表達式的上下文中是有效的),這個類型應該遵循一個特殊的模式:

  • 編譯器應該能找到一個叫GetAwaiter實例方法或擴展方法。這個方法的返回類型應該滿足某些條件:
  • 實現了INotifyCompletion接口。
  • bool IsCompleted {get;}屬性和T GetResult()方法。

這表示我們可以輕松地讓Lazy<T>變得“可等待”:

public struct LazyAwaiter<T> : INotifyCompletion
{
    private readonly Lazy<T> _lazy;
 
    public LazyAwaiter(Lazy<T> lazy) => _lazy = lazy;
 
    public T GetResult() => _lazy.Value;
 
    public bool IsCompleted => true;
 
    public void OnCompleted(Action continuation) { }
}
 
public static class LazyAwaiterExtensions
{
    public static LazyAwaiter<T> GetAwaiter<T>(this Lazy<T> lazy)
    {
        return new LazyAwaiter<T>(lazy);
    }
}
public static async Task Foo()
{
    var lazy = new Lazy<int>(() => 42);
    var result = await lazy;
    Console.WriteLine(result);
}

這個例子可能看起來過於人為,但這個可擴展性其實非常有用,並且會被運用在實際中。比如,Reactive Extensions for .NET就提供了一個自定義的awaiter用於在異步方法中等待(await)IObservable<T>的實例。BCL自己也有用於Task.YieldHopToThreadPoolAwaitableYieldAwaitable

public struct HopToThreadPoolAwaitable : INotifyCompletion
{
    public HopToThreadPoolAwaitable GetAwaiter() => this;
    public bool IsCompleted => false;
 
    public void OnCompleted(Action continuation) => Task.Run(continuation);
    public void GetResult() { }
}

下面的單元測試展示了上面的awaiter的運用:

[Test]
public async Task Test()
{
    var testThreadId = Thread.CurrentThread.ManagedThreadId;
    await Sample();
 
    async Task Sample()
    {
        Assert.AreEqual(Thread.CurrentThread.ManagedThreadId, testThreadId);
 
        await default(HopToThreadPoolAwaitable);
        Assert.AreNotEqual(Thread.CurrentThread.ManagedThreadId, testThreadId);
    }
}

其中的異步方法的第一部分(await語句之前的部分)是同步執行的。在大多數情況下,這是沒問題並且對“預先參數驗證(eager argument validation )”是必要的,但有時候我們希望確保方法的主體不會阻塞調用者的線程。HopToThreadPoolAwaitable確保了方法的剩余部分在一個線程池線程而不是調用者線程中執行。

“類任務”(Task-like)類型

從支持async/await的編譯器的第一個版本(即C# 5)開始,就可以自定義awaiter了。這個可擴展性十分有用但是卻是有限的,因為所有的異步方法都必須返回void, TaskTask<T>。從C# 7.2開始,編譯器支持“類任務”類型。

“類任務”類型是一個class或者struct,它與一個通過AsyncMethodBuilderAttribute標識的builder類型 相關聯。要使“類任務”類型有用,它應該像我們前面描述的awaiter那樣是可等待的。基本上,“類任務”類型結合了前面描述的兩種可擴展性的方法,並且使第一種方法得到了正式支持。

現在你還必須自己定義這個attribute,例子:my github repo

下面是一個簡單的例子,一個定義為struct的自定義“類任務”類型:

public sealed class TaskLikeMethodBuilder
{
    public TaskLikeMethodBuilder()
        => Console.WriteLine(".ctor");
 
    public static TaskLikeMethodBuilder Create()
        => new TaskLikeMethodBuilder();
 
    public void SetResult() => Console.WriteLine("SetResult");
 
    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine
    {
        Console.WriteLine("Start");
        stateMachine.MoveNext();
    }
 
    public TaskLike Task => default(TaskLike);
 
    // AwaitOnCompleted, AwaitUnsafeOnCompleted, SetException 
    // and SetStateMachine are empty

}
 
[System.Runtime.CompilerServices.AsyncMethodBuilder(typeof(TaskLikeMethodBuilder))]
public struct TaskLike
{
    public TaskLikeAwaiter GetAwaiter() => default(TaskLikeAwaiter);
}
 
public struct TaskLikeAwaiter : INotifyCompletion
{
    public void GetResult() { }
 
    public bool IsCompleted => true;
 
    public void OnCompleted(Action continuation) { }
}

現在我們便可以定義一個返回TaskLike類型的方法了,甚至可以在方法內部使用不同的“類任務”類型:

public async TaskLike FooAsync()
{
    await Task.Yield();
    await default(TaskLike);
}

使用“類任務”類型的主要原因是為了減少異步操作的開銷。每一個返回Task<T>的異步操作都至少在托管堆中分配了一個對象——這個任務本身。這對大多數應用程序來說都不是什么問題,特別是當他們處理粗粒度的異步操作時。但對可能會造成每秒執行數千個小任務的基礎結構級代碼來說,情況並非如此。對於這樣的場景,每次調用減少一次分配可以合理地提高性能。

異步模式可擴展性

  • C#編譯器提供了擴展異步方法的各種方式。
  • 你可以通過提供自己的AsyncTaskMethodBuilder類型來改變現有的基於Task的異步方法的行為。
  • 你可以通過實現“可等待模式(awaitable pattern)”使一個類型變得“可等待(awaitable)”。
  • 從C# 7開始你可以構建自己的“類任務”(task-like)類型。

其他參考資料

下次我們將討論異步方法的性能特點,我們將會看到最新的“類任務”(task-like)類型System.ValueTask是如何影響性能的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM