溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

.NET中異步的示例分析

發(fā)布時間:2021-06-15 09:14:39 來源:億速云 閱讀:150 作者:小新 欄目:開發(fā)技術(shù)

這篇文章主要介紹了.NET中異步的示例分析,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

一、前言

網(wǎng)上有很多關(guān)于 .NET  async/await 的介紹,但是很遺憾,很少有正確的,甚至說大多都是“從現(xiàn)象編原理”都不過分。

最典型的比如通過前后線程 ID 來推斷其工作方式、在 async 方法中用 Thread.Sleep 來解釋 Task 機制而導(dǎo)出多線程模型的結(jié)論、在 Task.Run 中包含 IO bound 任務(wù)來推出這是開了一個多線程在執(zhí)行任務(wù)的結(jié)論等等。

看上去似乎可以解釋的通,可是很遺憾,無論是從原理還是結(jié)論上看都是錯誤的。

要了解 .NET 中的 async/await 機制,首先需要有操作系統(tǒng)原理的基礎(chǔ),否則的話是很難理解清楚的,如果沒有這些基礎(chǔ)而試圖向他人解釋,大多也只是基于現(xiàn)象得到的錯誤猜想。

二、初看異步

說到異步大家應(yīng)該都很熟悉了,2012 年 C# 5 引入了新的異步機制:Task,并且還有兩個新的關(guān)鍵字 awaitasync,這已經(jīng)不是什么新鮮事了,而且如今這個異步機制已經(jīng)被各大語言借鑒,如 JavaScript、TypeScript、Rust、C++ 等等。

下面給出一個簡單的對照:

語言調(diào)度單位關(guān)鍵字/方法
C#Task<>、ValueTask<>async、await
C++std::future<>co_await
Ruststd::future::Future<>.await
JavaScript、TypeScriptPromise<>async、await

當(dāng)然,這里這并不是本文的重點,只是提一下,方便大家在有其他語言經(jīng)驗的情況下(如果有),可以認(rèn)識到 C# 中 Taskasync/await 究竟是一個和什么可以相提并論的東西。

三、多線程編程

在該異步編程模型誕生之前,多線程編程模型是很多人所熟知的。一般來說,開發(fā)者會使用 Threadstd::thread 之類的東西作為線程的調(diào)度單位來進行多線程開發(fā),每一個這樣的結(jié)構(gòu)表示一個對等線程,線程之間采用互斥或者信號量等方式進行同步。

多線程對于科學(xué)計算速度提升等方面效果顯著,但是對于 IO 負(fù)荷的任務(wù),例如從讀取文件或者 TCP 流,大多數(shù)方案只是分配一個線程進行讀取,讀取過程中阻塞該線程:

void Main()
{
    while (true)
    {
        var client = socket.Accept();
        new Thread(() => ClientThread(client)).Start();
    }
}

void ClientThread(Socket client)
{
    var buffer = new byte[1024];
    while (...)
    {
        // read and block
        client.Read(buffer, 0, 1024); 
    }
}

上述代碼中,Main 函數(shù)在接收客戶端之后即分配了一個新的用戶線程用于處理該客戶端,從客戶端接收數(shù)據(jù)。client.Read() 執(zhí)行后,該線程即被阻塞,即使阻塞期間該線程沒有任何的操作,該用戶線程也不會被釋放,并被操作系統(tǒng)不斷輪轉(zhuǎn)調(diào)度,這顯然浪費了資源。

另外,如果線程數(shù)量多起來,頻繁在不同線程之間輪轉(zhuǎn)切換上下文,線程的上下文也不小,會浪費掉大量的性能。

四、異步編程

因此對于此工作內(nèi)容(IO),我們在 Linux 上有了 epoll/io_uring 技術(shù),在 Windows 上有了 IOCP 技術(shù)用以實現(xiàn)異步 IO 操作。

(這里插句題外話,吐槽一句,Linux 終于知道從 Windows 抄作業(yè)了。先前的 epoll 對比 IOCP 簡直不能打,被 IOCP 全面打壓,io_uring 出來了才好不容易能追上 IOCP,不過 IOCP 從 Windows Vista 時代開始每一代都有很大的優(yōu)化,io_uring 能不能追得上還有待商榷)

這類 API 有一個共同的特性就是,在操作 IO 的時候,調(diào)用方控制權(quán)被讓出,等待 IO 操作完成之后恢復(fù)先前的上下文,重新被調(diào)度繼續(xù)運行。

所以表現(xiàn)就是這樣的:

假設(shè)我現(xiàn)在需要從某設(shè)備中讀取 1024 個字節(jié)長度的數(shù)據(jù),于是我們將緩沖區(qū)的地址和內(nèi)容長度等信息封裝好傳遞給操作系統(tǒng)之后我們就不管了,讀取什么的讓操作系統(tǒng)去做就好了。

操作系統(tǒng)在內(nèi)核態(tài)下利用 DMA 等方式將數(shù)據(jù)讀取了 1024 個字節(jié)并寫入到我們先前的 buffer 地址下,然后切換到用戶態(tài)將從我們先前讓出控制權(quán)的位置,對其進行調(diào)度使其繼續(xù)執(zhí)行。

你可以發(fā)現(xiàn)這么一來,在讀取數(shù)據(jù)期間就沒有任何的線程被阻塞,也不存在被頻繁調(diào)度和切換上下文的情況,只有當(dāng) IO 操作完成之后才會被重新調(diào)度并恢復(fù)先前讓出控制權(quán)時的上下文,使得后面的代碼繼續(xù)執(zhí)行。

當(dāng)然,這里說的是操作系統(tǒng)的異步 IO 實現(xiàn)方式,以便于讀者對異步這個行為本身進行理解,和 .NET 中的異步還是有區(qū)別,Task 本身和操作系統(tǒng)也沒什么關(guān)系。

五、Task (ValueTask)

說了這么久還是沒有解釋 Task 到底是個什么東西,從上面的分析就可以得出,Task 其實就是一個所謂的調(diào)度單位,每個異步任務(wù)被封裝為一個 Task 在 CLR 中被調(diào)度,而 Task 本身會運行在 CLR 中的預(yù)先分配好的線程池中。

總有很多人因為 Task 借助線程池執(zhí)行而把 Task 歸結(jié)為多線程模型,這是完全錯誤的。

這個時候有人跳出來了,說:你看下面這個代碼

static async Task Main()
{
    while (true)
    {
        Console.WriteLine(Environment.CurrentManagedThreadId);
        await Task.Delay(1000);
    }
}

輸出的線程 ID 不一樣欸,你騙人,這明明就是多線程!對于這種言論,我也只能說這些人從原理上理解的就是錯誤的。

當(dāng)代碼執(zhí)行到 await 的時候,此時當(dāng)前的控制權(quán)就已經(jīng)被讓出了,當(dāng)前線程并沒有在阻塞地等待延時結(jié)束;待 Task.Delay() 完畢后,CLR 從線程池當(dāng)中挑起了一個先前分配好的已有的但是空閑的線程,將讓出控制權(quán)前的上下文信息恢復(fù),使得該線程恰好可以從先前讓出的位置繼續(xù)執(zhí)行下去。這個時候,可能挑到了先前讓出前所在的那個線程,導(dǎo)致前后線程 ID 一致;也有可能挑到了另外一個和之前不一樣的線程執(zhí)行下面的代碼,使得前后的線程 ID 不一致。在此過程中并沒有任何的新線程被分配了出去。

當(dāng)然,在 WPF 等地方,因為利用了 SynchronizationContext 對調(diào)度行為進行了控制,所以可以得到和上述不同的結(jié)論,和這個相關(guān)的還有 .ConfigureAwait() 的用法,但是這里不是本文重點,因此就不做展開。

在 .NET 中由于采用 stackless 的做法,這里需要用到 CPS 變換,大概是這么個流程:

using System;
using System.Threading.Tasks;

public class C 
{
    public async Task M()
    {
        var a = 1;
        await Task.Delay(1000);
        Console.WriteLine(a);
    }
}

編譯后:

public class C
{
    [StructLayout(LayoutKind.Auto)]
    [CompilerGenerated]
    private struct <M>d__0 : IAsyncStateMachine
    {
        public int <>1__state;

        public AsyncTaskMethodBuilder <>t__builder;

        private int <a>5__2;

        private TaskAwaiter <>u__1;

        private void MoveNext()
        {
            int num = <>1__state;
            try
            {
                TaskAwaiter awaiter;
                if (num != 0)
                {
                    <a>5__2 = 1;
                    awaiter = Task.Delay(1000).GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        num = (<>1__state = 0);
                        <>u__1 = awaiter;
                        <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
                        return;
                    }
                }
                else
                {
                    awaiter = <>u__1;
                    <>u__1 = default(TaskAwaiter);
                    num = (<>1__state = -1);
                }
                awaiter.GetResult();
                Console.WriteLine(<a>5__2);
            }
            catch (Exception exception)
            {
                <>1__state = -2;
                <>t__builder.SetException(exception);
                return;
            }
            <>1__state = -2;
            <>t__builder.SetResult();
        }

        void IAsyncStateMachine.MoveNext()
        {
            //ILSpy generated this explicit interface implementation from .override directive in MoveNext
            this.MoveNext();
        }

        [DebuggerHidden]
        private void SetStateMachine(IAsyncStateMachine stateMachine)
        {
            <>t__builder.SetStateMachine(stateMachine);
        }

        void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
        {
            //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
            this.SetStateMachine(stateMachine);
        }
    }

    [AsyncStateMachine(typeof(<M>d__0))]
    public Task M()
    {
        <M>d__0 stateMachine = default(<M>d__0);
        stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
        stateMachine.<>1__state = -1;
        stateMachine.<>t__builder.Start(ref stateMachine);
        return stateMachine.<>t__builder.Task;
    }
}

可以看到,原來的變量 a 被塞到了 <a>5__2 里面去(相當(dāng)于備份上下文),Task 狀態(tài)的轉(zhuǎn)換后也是靠著調(diào)用 MoveNext(相當(dāng)于狀態(tài)轉(zhuǎn)換后被重新調(diào)度)來接著驅(qū)動代碼執(zhí)行的,里面的 num 就表示當(dāng)前的狀態(tài),num 如果為 0 表示 Task 完成了,于是接著執(zhí)行下面的代碼 Console.WriteLine(<a>5__2);。

但是上面和經(jīng)典的多線程編程的那一套一樣嗎?不一樣。

至于 ValueTask 是個什么玩意,官方發(fā)現(xiàn),Task 由于本身是一個 class,在運行時如果頻繁反復(fù)的分配和回收會給 GC 造成不小的壓力,因此出了一個 ValueTask,這個東西是 struct,分配在棧上,這樣的話就不會給 GC 造成壓力了,減輕了開銷。不過也正因為 ValueTask 是會在棧上分配的值類型結(jié)構(gòu),因此提供的功能也不如 Task 全面。

六、Task.Run

由于 .NET 是允許有多個線程的,因此也提供了 Task.Run 這個方法,允許我們將 CPU bound 的任務(wù)放在上述的線程池之中的某個線程上執(zhí)行,并且允許我們將該負(fù)載作為一個 Task 進行管理,僅在這一點才和多線程的采用線程池的編程比較像。

對于瀏覽器環(huán)境(v8),這個時候是完全沒有多線程這一說的,因此你開的新的 Promise 其實是后面利用事件循環(huán)機制,將該微任務(wù)以異步的方式執(zhí)行。

想一想在 JavaScript 中,Promise 是怎么用的:

let p = new Promise((resolve, reject) => {
    // do something
    let success = true;
    let result = 123456;

    if (success) {
        resolve(result);
    }
    else {
        reject("failed");
    }
})

然后調(diào)用:

let r = await p;
console.log(r); // 輸出 123456

你只需要把這一套背后的驅(qū)動器:事件循環(huán)隊列,替換成 CLR 的線程池,就差不多是 .NET 的 Task 相對 JavaScript 的 Promise 的工作方式了。

如果你把 CLR 線程池線程數(shù)量設(shè)置為 1,那就和 JavaScript 這套幾乎差不多了(雖然實現(xiàn)上還是有差異)。

這時有人要問了:“我在 Task.Run 里面套了好幾層 Task.Run,可是為什么層數(shù)深了之后里面的不執(zhí)行了呢?” 這是因為上面所說的線程池被耗盡了,后面的 Task 還在排著隊等待被調(diào)度。

七、自己封裝異步邏輯

了解了上面的東西之后,相信對 .NET 中的異步機制應(yīng)該理解得差不多了,可以看出來這一套是名副其實的 coroutine,并且在實現(xiàn)上是 stackless 的。至于有的人說的什么狀態(tài)機什么的,只是實現(xiàn)過程中利用的手段而已,并不是什么重要的東西。

那我們要怎么樣使用 Task 來編寫我們自己的異步代碼呢?

事件驅(qū)動其實也可以算是一種異步模型,例如以下情景:

A 函數(shù)調(diào)用 B 函數(shù),調(diào)用發(fā)起后就直接返回不管了(BeginInvoke),B 函數(shù)執(zhí)行完成后觸發(fā)事件執(zhí)行 C 函數(shù)。

private event Action CompletedEvent;

void A()
{
    CompletedEvent += C;
    Console.WriteLine("begin");
    ((Action)B).BeginInvoke();
}

void B()
{
    Console.WriteLine("running");
    CompletedEvent?.Invoke();
}

void C()
{
    Console.WriteLine("end");
}

那么我們現(xiàn)在想要做一件事,就是把上面的事件驅(qū)動改造為利用 async/await 的異步編程模型,改造后的代碼就是簡單的:

async Task A()
{
    Console.WriteLine("begin");
    await B();
    Console.WriteLine("end");
}

Task B()
{
    Console.WriteLine("running");
    return Task.CompletedTask;
}

你可以看到,原本 C 函數(shù)的內(nèi)容被放到了 A 調(diào)用 B 的下面,為什么呢?其實很簡單,因為這里 await B(); 這一行以后的內(nèi)容,本身就可以理解為 B 函數(shù)的回調(diào)了,只不過在內(nèi)部實現(xiàn)上,不是直接從 B 進行調(diào)用的回調(diào),而是 A 先讓出控制權(quán),B 執(zhí)行完成后,CLR 切換上下文,將 A 調(diào)度回來繼續(xù)執(zhí)行剩下的代碼。

如果事件相關(guān)的代碼已經(jīng)確定不可改動(即不能改動 B 函數(shù)),我們想將其封裝為異步調(diào)用的模式,那只需要利用 TaskCompletionSource 即可:

private event Action CompletedEvent;

async Task A()
{
    // 因為 TaskCompletionSource 要求必須有一個泛型參數(shù)
    // 因此就隨便指定了一個 bool
    // 本例中其實是不需要這樣的一個結(jié)果的
    // 需要注意的是從 .NET 5 開始
    // TaskCompletionSource 不再強制需要泛型參數(shù)
    var tsc = new TaskCompletionSource<bool>();
    // 隨便寫一個結(jié)果作為 Task 的結(jié)果
    CompletedEvent += () => tsc.SetResult(false);

    Console.WriteLine("begin");
    ((Action)B).BeginInvoke();
    await tsc.Task;
    Console.WriteLine("end");
}

void B()
{
    Console.WriteLine("running");
    CompletedEvent?.Invoke();
}

順便提一句,這個 TaskCompletionSource<T> 其實和 JavaScript 中的 Promise<T> 更像。SetResult() 方法對應(yīng) resolve(),SetException() 方法對應(yīng) reject()。.NET 比 JavaScript 還多了一個取消狀態(tài),因此還可以 SetCancelled() 表示任務(wù)被取消了。

八、同步方式調(diào)用異步代碼

說句真的,一般能有這個需求,都說明你的代碼寫的有問題,但是如果你無論如何都想以阻塞的方式去等待一個異步任務(wù)完成的話:

Task t = ...
t.GetAwaiter().GetResult();

祝你好運,這相當(dāng)于,t 中的異步任務(wù)開始執(zhí)行后,你將當(dāng)前線程阻塞,然后等到 t 完成之后再喚醒,可以說是:毫無意義,而且很有可能因為代碼編寫不當(dāng)而導(dǎo)致死鎖的發(fā)生。

九、void async 是什么?

最后有人會問了,函數(shù)可以寫 async Task Foo(),還可以寫 async void Bar(),這有什么區(qū)別呢?

對于上述代碼,我們一般調(diào)用的時候,分別這么寫:

await Foo();
Bar();

可以發(fā)現(xiàn),誒這個 Bar 函數(shù)不需要 await 誒。為什么呢?

其實這和用以下方式調(diào)用 Foo 是一樣的:

_ = Foo();

換句話說就是調(diào)用后瞬間就直接拋掉不管了,不過這樣你也就沒法知道這個異步任務(wù)的狀態(tài)和結(jié)果了。

十、await 必須配合 Task/ValueTask 才能用嗎?

當(dāng)然不是。

在 C# 中只要你的類中包含 GetAwaiter() 方法和 bool IsCompleted 屬性,并且 GetAwaiter() 返回的東西包含一個 GetResult() 方法、一個 bool IsCompleted 屬性和實現(xiàn)了 INotifyCompletion,那么這個類的對象就是可以 await 的。

public class MyTask<T>
{
    public MyAwaiter<T> GetAwaiter()
    {
        return new MyAwaiter<T>();
    }
}

public class MyAwaiter<T> : INotifyCompletion
{
    public bool IsCompleted { get; private set; }
    public T GetResult()
    {
        throw new NotImplementedException();
    }
    public void OnCompleted(Action continuation)
    {
        throw new NotImplementedException();
    }
}

public class Program
{
    static async Task Main(string[] args)
    {
        var obj = new MyTask<int>();
        await obj;
    }
}

感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“.NET中異步的示例分析”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識等著你來學(xué)習(xí)!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI