.NET:用於處理多執行緒和非同步的工具。 第1部分

我正在發表關於 Habr 的原始文章,其翻譯發佈在公司 блоге.

在電腦出現之前,就存在非同步執行某些操作而不需要立即等待結果的需求,或將大量工作分配給多個執行它的單元的需求。 隨著它們的出現,這種需求變得非常明顯。 現在,2019 年,我正在一台配備 8 核心 Intel Core 處理器的筆記型電腦上寫這篇文章,該處理器上有一百多個進程並行運行,甚至還有更多線程。 旁邊有一支略顯破舊的手機,幾年前買的,搭載8核心處理器。 主題資源充滿了文章和視頻,作者對今年配備 16 核處理器的旗艦智慧型手機表示讚賞。 MS Azure 以不到 20 美元/小時的價格提供具有 128 核心處理器和 2 TB RAM 的虛擬機器。 不幸的是,如果無法管理線程的交互,就不可能最大限度地發揮和利用這種能力。

術語

流程 - OS對象,隔離的位址空間,包含執行緒。
- 作業系統對象,最小的執行單元,行程的一部分,執行緒在行程內相互共享記憶體和其他資源。
多任務處理 - 作業系統屬性,同時運行多個行程的能力
多核心 - 處理器的屬性,使用多個核心進行資料處理的能力
多重處理 - 電腦的一個屬性,即物理上同時與多個處理器一起工作的能力
多執行緒 — 行程的屬性,在多個執行緒之間分配資料處理的能力。
平行性 - 在單位時間內同時執行多個物理動作
異步 — 執行操作而不等待該處理完成;執行結果可以稍後處理。

隱喻

並不是所有的定義都是好的,有些還需要額外的解釋,所以我會在正式引入的術語中添加一個關於做早餐的比喻。 在這個比喻中,做早餐是一個過程。

早上準備早餐時我(中央處理器)我來到廚房(計算機)。 我有2隻手(核心)。 廚房裡有很多設備(IO):烤箱、水壺、烤麵包機、冰箱。 我打開瓦斯,在上面放一個煎鍋,然後倒入油,不等它加熱(異步、非阻塞 IO 等待),我把雞蛋從冰箱拿出來,打散到盤子裡,然後用一隻手敲打(線程#1)和第二個(線程#2)拿著盤子(共享資源)。 現在我想開水壺,但我的手不夠(線程飢餓)在此期間,煎鍋加熱(處理結果),我將攪打過的東西倒入其中。 我伸手拿起水壺,打開它,愚蠢地看著裡面的水沸騰(阻塞-IO-等待),儘管在這段時間裡他可以清洗他攪打煎蛋捲的盤子。

我只用兩隻手煮了一個煎蛋,而且我沒有更多的手,但同時,在攪打煎蛋的那一刻,同時進行了2個操作:攪拌煎蛋、握住盤子、加熱煎鍋CPU是電腦中最快的部分,IO是最常變慢的部分,因此通常有效的解決方案是在從IO接收資料的同時用某些東西佔用CPU。

繼續比喻:

  • 如果在準備蛋捲的過程中,我也會嘗試換衣服,這就是一個多工的例子。 一個重要的細微差別是:電腦在這方面比人類做得更好。
  • 擁有多名廚師的廚房,例如在餐廳中 - 多核計算機。
  • 購物中心美食廣場的許多餐廳 — 資料中心

.NET 工具

.NET 擅長處理線程,就像處理許多其他事情一樣。 每個新版本都引進了越來越多的新工具,以及作業系統執行緒上的新抽象層。 在建構抽象時,框架開發人員使用一種方法,在使用高階抽象時,有機會深入到下面的一個或多個層級。 大多數情況下,這是沒有必要的,事實上,它打開了用霰彈槍搬起石頭砸自己腳的大門,但有時,在極少數情況下,這可能是解決當前抽象級別未解決的問題的唯一方法。

我所說的工具,是指框架和第三方包提供的應用程式介面(API),以及簡化搜尋與多執行緒程式碼相關的任何問題的整個軟體解決方案。

啟動一個執行緒

Thread 類別是 .NET 中用來處理執行緒的最基本的類別。 建構函式接受兩個委託之一:

  • ThreadStart — 無參數
  • ParametrizedThreadStart - 具有一個物件類型的參數。

委託將在呼叫 Start 方法後在新建立的執行緒中執行。如果將 ParametrizedThreadStart 類型的委託傳遞給建構函數,則必須將物件傳遞給 Start 方法。 需要此機制將任何本地資訊傳輸到串流。 值得注意的是,創建線程是一個昂貴的操作,並且線程本身是一個重對象,至少因為它在堆疊上分配1MB內存並且需要與OS API交互。

new Thread(...).Start(...);

ThreadPool 類別代表池的概念。 在 .NET 中,執行緒池是一項工程,Microsoft 的開發人員投入了大量精力來確保它在各種場景中都能以最佳方式運作。

一般概念:

從應用程式啟動的那一刻起,它就會在後台創建多個預留線程,並提供使用它們的能力。 如果頻繁且大量使用線程,則池會擴展以滿足呼叫者的需求。 當池中在適當的時候沒有空閒線程時,它將等待其中一個線程返回,或者創建一個新線程。 由此可見,執行緒池非常適合某些短期操作,但不太適合在整個應用程式操作中作為服務運行的操作。

要使用池中的線程,有一個 QueueUserWorkItem 方法接受 WaitCallback 類型的委託,該方法與 ParametrizedThreadStart 具有相同的簽名,並且傳遞給它的參數執行相同的功能。

ThreadPool.QueueUserWorkItem(...);

不太為人所知的線程池方法RegisterWaitForSingleObject用於組織非阻塞IO操作。 當傳遞給該方法的 WaitHandle 為「已釋放」時,將呼叫傳遞給該方法的委託。

ThreadPool.RegisterWaitForSingleObject(...)

.NET 有一個執行緒計時器,它與 WinForms/WPF 計時器的不同之處在於,它的處理程序將在從池中取得的執行緒上呼叫。

System.Threading.Timer

還有一個相當奇特的方法可以將執行委託發送到池中的線程 - BeginInvoke 方法。

DelegateInstance.BeginInvoke

我想簡單介紹一下可以呼叫上述許多方法的函數 - 來自 Kernel32.dll Win32 API 的 CreateThread。 借助 extern 方法的機制,有一種方法可以呼叫此函數。 我在一個可怕的遺留程式碼範例中只見過一次這樣的調用,而這樣做的作者的動機對我來說仍然是個謎。

Kernel32.dll CreateThread

查看和調試線程

您建立的執行緒、所有第三方元件以及.NET 池都可以在 Visual Studio 的「執行緒」視窗中檢視。 只有當應用程式處於偵錯狀態且處於中斷模式時,此視窗才會顯示執行緒資訊。 在這裡您可以輕鬆地查看每個執行緒的堆疊名稱和優先級,並將偵錯切換到特定執行緒。 使用 Thread 類別的 Priority 屬性,您可以設定執行緒的優先權,OC 和 CLR 在執行緒之間分配處理器時間時會將其視為建議。

.NET:用於處理多執行緒和非同步的工具。 第1部分

任務並行庫

任務並行庫 (TPL) 是在 .NET 4.0 中引入的。 現在它是處理非同步的標準和主要工具。 任何使用舊方法的程式碼都被視為遺留程式碼。 TPL 的基本單元是 System.Threading.Tasks 命名空間中的 Task 類別。 任務是線程的抽象。 使用新版本的 C# 語言,我們獲得了一種處理任務的優雅方式 - 非同步/等待運算子。 這些概念使得編寫非同步程式碼成為可能,就好像它是簡單和同步的一樣,這使得即使對線程內部工作了解甚少的人也可以編寫使用它們的應用程序,這些應用程序在執行長時間操作時不會凍結。 使用 async/await 是一篇甚至幾篇文章的主題,但我將嘗試用幾句話來了解它的要點:

  • async 是傳回 Task 或 void 的方法的修飾符
  • 而await是一個非阻塞的任務等待操作符。

再次強調:await操作符,一般情況下(也有例外),會進一步釋放當前執行的線程,當Task執行完畢,線程(其實應該說上下文會更正確) ,但稍後會詳細介紹)將繼續進一步執行該方法。 在.NET內部,這種機制的實作方式與yield return相同,即編寫的方法變成一個完整的類,它是一個狀態機,可以根據這些狀態以單獨的部分執行。 任何有興趣的人都可以使用 async/await 編寫任何簡單的程式碼,使用啟用了編譯器產生程式碼的 JetBrains dotPeek 來編譯和查看程式集。

讓我們看看啟動和使用任務的選項。 在下面的程式碼範例中,我們建立了一個不執行任何有用操作的新任務(線程.睡眠(10000)),但在現實生活中這應該是一些複雜的 CPU 密集型工作。

using TCO = System.Threading.Tasks.TaskCreationOptions;

public static async void VoidAsyncMethod() {
    var cancellationSource = new CancellationTokenSource();

    await Task.Factory.StartNew(
        // Code of action will be executed on other context
        () => Thread.Sleep(10000),
        cancellationSource.Token,
        TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness,
        scheduler
    );

    //  Code after await will be executed on captured context
}

建立的任務具有多個選項:

  • LongRunning 暗示該任務不會很快完成,這意味著可能值得考慮不從池中取出線程,而是為此任務創建一個單獨的線程,以免傷害其他人。
  • AttachedToParent - 任務可以依層次結構排列。 如果使用此選項,則任務可能處於本身已完成並等待其子層級執行的狀態。
  • PreferFairness - 意味著最好先執行較早發送的任務,然後再執行較晚發送的任務。 但這只是建議,並不能保證結果。

傳遞給方法的第二個參數是 CancellationToken。 為了在操作開始後正確處理操作的取消,正在執行的程式碼必須充滿 CancellationToken 狀態的檢查。 如果沒有檢查,則對 CancellationTokenSource 物件呼叫的 Cancel 方法將只能在任務開始之前停止任務的執行。

最後一個參數是 TaskScheduler 類型的調度程序物件。 此類及其後代旨在控制跨線程分配任務的策略;預設情況下,任務將在池中的隨機線程上執行。

await 運算子應用於建立的 Task,這表示在其之後編寫的程式碼(如果有)將在與 wait 之前的程式碼相同的上下文中(通常這表示在同一執行緒上)執行。

該方法被標記為async void,這意味著它可以使用await運算符,但呼叫程式碼將無法等待執行。 如果需要這樣的功能,則該方法必須傳回 Task。 標記為 async void 的方法非常常見:通常,這些是事件處理程序或其他遵循即發即忘原則的方法。 如果你不僅需要給出等待執行結束的機會,而且還需要回傳結果,那麼你需要使用Task。

在 StartNew 方法傳回的任務以及任何其他任務上,您可以使用 false 參數呼叫ConfigureAwait 方法,然後await 之後的執行將不會在擷取的上下文上繼續執行,而是在任意上下文上繼續執行。 當執行上下文對於等待之後的程式碼不重要時,應始終執行此操作。 這也是 MS 在編寫將打包在庫中交付的程式碼時的建議。

讓我們詳細討論如何等待任務完成。 以下是一個程式碼範例,其中包含有關何時有條件地完成期望以及何時有條件地完成期望的註釋。

public static async void AnotherMethod() {

    int result = await AsyncMethod(); // good

    result = AsyncMethod().Result; // bad

    AsyncMethod().Wait(); // bad

    IEnumerable<Task> tasks = new Task[] {
        AsyncMethod(), OtherAsyncMethod()
    };

    await Task.WhenAll(tasks); // good
    await Task.WhenAny(tasks); // good

    Task.WaitAll(tasks.ToArray()); // bad
}

在第一個範例中,我們等待任務完成,而不阻塞呼叫執行緒;只有當結果已經存在時,我們才會傳回處理結果;在此之前,呼叫執行緒將自行處理。

在第二個選項中,我們阻塞呼叫線程,直到計算出方法的結果。 這不僅是因為我們佔用了一個線程這樣寶貴的程式資源,簡單的閒置,還因為如果我們調用的方法的程式碼中包含await,並且同步上下文需要在之後返回到調用線程等待,那麼我們就會陷入死鎖:呼叫執行緒等待非同步方法的結果被計算出來,非同步方法徒勞地嘗試在呼叫執行緒中繼續執行。

這種方法的另一個缺點是複雜的錯誤處理。 事實上,使用 async/await 時非同步程式碼中的錯誤非常容易處理 - 它們的行為與同步程式碼相同。 而如果我們對任務應用同步等待驅魔,原始異常就會變成 AggregateException,即要處理異常,您必須檢查 InnerException 類型,並在一個 catch 區塊內自行編寫一個 if 鏈,或使用 catch while 構造,而不是使用 C# 世界中更熟悉的 catch 區塊鏈。

第三個也是最後一個例子也因相同的原因被標記為“壞”,並且包含所有相同的問題。

WhenAny 和 WhenAll 方法對於等待一組任務非常方便;它們將一組任務包裝成一個,當該組中的任務首次觸發時,或者當所有任務都完成執行時,該方法將觸發。

停止線程

由於各種原因,可能需要在流程開始後停止流程。 有多種方法可以做到這一點。 Thread 類別有兩個適當命名的方法: 流產 и 打斷。 強烈不建議使用第一個,因為在任意時刻呼叫它之後,在處理任何指令的過程中,都會拋出異常 執行緒中止異常。 您不希望在遞增任何整數變數時拋出這樣的異常,對吧? 而當使用這個方法的時候,這是一個非常真實的情況。 如果需要防止 CLR 在某段程式碼中產生此類異常,可以將其包裝在呼叫中 Thread.BeginCriticalRegion, Thread.EndCriticalRegion。 在finally 區塊中編寫的任何程式碼都包含在此類呼叫中。 因此,在框架程式碼的深處,您可以找到有空 try 的區塊,但找不到有空 finally 的區塊。 Microsoft 非常不鼓勵這種方法,因此他們沒有將其包含在 .net core 中。

中斷方法的工作方式更可預測。 它可以透過異常中斷線程 線程中斷例外 僅在執行緒處於等待狀態的那些時刻。 當等待 WaitHandle、鎖定或呼叫 Thread.Sleep 後掛起時,它會進入此狀態。

由於不可預測性,上述兩種選擇都很糟糕。 解決方案是使用結構體 取消令牌 和班級 取消令牌來源。 重點是:建立了 CancellationTokenSource 類別的實例,只有擁有它的人才能透過呼叫該方法來停止操作 取消。 僅 CancellationToken 會傳遞給操作本身。 CancellationToken 持有者無法自行取消操作,只能檢查操作是否已取消。 有一個布林屬性用於此 是否要求取消 和方法 如果取消請求則拋出。 後者會拋出例外 任務取消異常 如果在鸚鵡學舌的 CancellationToken 實例上呼叫 Cancel 方法。 這是我推薦使用的方法。 這是對先前選項的改進,可以完全控制異常操作可以中止的時間點。

停止執行緒最殘酷的選擇是呼叫 Win32 API TerminateThread 函數。 呼叫此函數後 CLR 的行為可能是不可預測的。 MSDN 上關於這個函數的描述如下: 「TerminateThread 是一個危險的函數,只應在最極端的情況下使用。 “

使用 FromAsync 方法將舊版 API 轉換為基於任務

如果您夠幸運,能夠從事一個在引入任務後開始的項目,並且不再給大多數開發人員帶來安靜的恐懼,那麼您將不必處理大量舊的API,無論是第三方的還是您團隊的API過去曾受過折磨。 幸運的是,.NET Framework 團隊照顧了我們,儘管目標可能是照顧我們自己。 儘管如此,.NET 有許多工具可以輕鬆地將用舊的非同步程式設計方法編寫的程式碼轉換為新的非同步程式設計方法。 其中之一是 TaskFactory 的 FromAsync 方法。 在下面的程式碼範例中,我使用此方法將 WebRequest 類別的舊非同步方法包裝在任務中。

object state = null;
WebRequest wr = WebRequest.CreateHttp("http://github.com");
await Task.Factory.FromAsync(
    wr.BeginGetResponse,
    we.EndGetResponse
);

這只是一個範例,您不太可能必須使用內建類型執行此操作,但任何舊專案都充滿了傳回 IAsyncResult 的 BeginDoSomething 方法和接收它的 EndDoSomething 方法。

使用 TaskCompletionSource 類別將舊版 API 轉換為基於任務

另一個需要考慮的重要工具是類 任務完成來源。 從功能、用途和操作原理來看,可能有點讓人想起我上面寫的ThreadPool類別的RegisterWaitForSingleObject方法。 使用此類,您可以輕鬆方便地將舊的非同步 API 包裝在任務中。

您可能會說,我已經討論了用於這些目的的 TaskFactory 類別的 FromAsync 方法。 這裡我們必須記住微軟在過去 15 年中提供的 .net 非同步模型開發的整個歷史:在基於任務的非同步模式(TAP)之前,有非同步程式設計模式(APP),它是關於方法的 開始做某事返回 IAsync結果 和方法 結束接受它的 DoSomething 以及這些年來的遺留下來的 FromAsync 方法非常完美,但隨著時間的推移,它被基於事件的非同步模式所取代(EAP),它假設非同步操作完成時將引發事件。

TaskCompletionSource 非常適合包裝圍繞事件模型建構的任務和遺留 API。 其工作本質是這樣的:該類別的一個物件有一個類型為Task的公共屬性,可以透過TaskCompletionSource類別的SetResult、SetException等方法控制其狀態。 在將await 運算子應用於此任務的地方,它將被執行或失敗並出現異常,這取決於應用於TaskCompletionSource 的方法。 如果還不清楚,讓我們看一下這個程式碼範例,其中一些舊的 EAP API 使用 TaskCompletionSource 包裝在任務中:當事件觸發時,任務將轉移到 Completed 狀態,並且應用了等待運算符的方法此任務將在收到對象後恢復執行 導致.

public static Task<Result> DoAsync(this SomeApiInstance someApiObj) {

    var completionSource = new TaskCompletionSource<Result>();
    someApiObj.Done += 
        result => completionSource.SetResult(result);
    someApiObj.Do();

    result completionSource.Task;
}

TaskCompletionSource 提示與技巧

包裝舊的 API 並不是使用 TaskCompletionSource 可以完成的全部工作。 使用此類為在不佔用線程的任務上設計各種 API 提供了一個有趣的可能性。 正如我們所記得的,流是一種昂貴的資源,而且它們的數量是有限的(主要受 RAM 量的限制)。 例如,透過開發具有複雜業務邏輯的載入 Web 應用程序,可以輕鬆實現此限制。 讓我們考慮一下我在實現長輪詢這樣的技巧時所討論的可能性。

簡而言之,這個技巧的本質是這樣的:你需要從 API 接收其自身發生的一些事件的訊息,而 API 由於某種原因無法報告事件,而只能返回狀態。 其中一個例子是,在 WebSocket 時代之前或由於某種原因不可能使用該技術時,所有 API 都建構在 HTTP 之上。 客戶端可以詢問HTTP伺服器。 HTTP 伺服器本身無法發起與客戶端的通訊。 一個簡單的解決方案是使用計時器輪詢伺服器,但這會給伺服器帶來額外的負載,並且平均TimerInterval / 2 會產生額外的延遲。為了解決這個問題,發明了一種稱為長輪詢的技巧,其中涉及延遲來自伺服器的回應伺服器直到超時到期或發生事件。 如果事件已經發生,則處理該事件,如果沒有發生,則再次發送請求。

while(!eventOccures && !timeoutExceeded)  {

  CheckTimout();
  CheckEvent();
  Thread.Sleep(1);
}

但一旦等待事件的客戶數量增加,這樣的解決方案就會被證明是糟糕的,因為... 每個這樣的客戶端佔用一個完整的線程來等待事件。 是的,當事件被觸發時,我們會得到額外的 1 毫秒延遲,大多數情況下這並不重要,但為什麼要讓軟體變得比實際情況更糟呢? 如果我們刪除 Thread.Sleep(1),那麼我們將徒勞地載入一個處理器核心 100% 空閒,在無用的循環中旋轉。 使用 TaskCompletionSource 您可以輕鬆地重新編寫此程式碼並解決上面確定的所有問題:

class LongPollingApi {

    private Dictionary<int, TaskCompletionSource<Msg>> tasks;

    public async Task<Msg> AcceptMessageAsync(int userId, int duration) {

        var cs = new TaskCompletionSource<Msg>();
        tasks[userId] = cs;
        await Task.WhenAny(Task.Delay(duration), cs.Task);
        return cs.Task.IsCompleted ? cs.Task.Result : null;
    }

    public void SendMessage(int userId, Msg m) {

        if (tasks.TryGetValue(userId, out var completionSource))
            completionSource.SetResult(m);
    }
}

此程式碼尚未準備好用於生產,而只是一個演示。 要在實際情況中使用它,您至少還需要處理訊息在無人預料的情況下到達的情況:在這種情況下,AsseptMessageAsync 方法應該傳回一個已經完成的任務。 如果這是最常見的情況,那麼您可以考慮使用 ValueTask。

當我們收到訊息請求時,我們建立一個 TaskCompletionSource 並將其放入字典中,然後等待首先發生的事情:指定的時間間隔到期或收到訊息。

ValueTask:原因與方式

async/await 運算符,如yield return 運算符,從方法產生狀態機,這是新物件的創建,這幾乎總是不重要,但在極少數情況下可能會產生問題。 這種情況可能是一個呼叫非常頻繁的方法,我們談論的是每秒數萬、數十萬次呼叫。 如果這樣的方法的編寫方式在大多數情況下會繞過所有等待方法返回結果,那麼.NET 提供了一個工具來最佳化它 - ValueTask 結構。 為了清楚起見,讓我們看一個它的使用範例:有一個我們經常造訪的快取。 裡面有一些值,然後我們簡單地回傳它們;如果沒有,那麼我們就去一些慢速的IO來取得它們。 我想異步執行後者,這意味著整個方法結果是異步的。 因此,編寫該方法的明顯方式如下:

public async Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return val;
    return await RequestById(id);
}

由於想要稍微優化一下,並且有點擔心 Roslyn 在編譯此程式碼時會產生什麼,因此您可以如下重寫此範例:

public Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return Task.FromResult(val);
    return RequestById(id);
}

事實上,這種情況下的最佳解決方案是優化熱路徑,即從字典中獲取值,而不需要任何不必要的分配和GC 上的負載,而在極少數情況下,我們仍然需要去IO取得數據,一切都將保持舊方式的加/減:

public ValueTask<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return new ValueTask<string>(val);
    return new ValueTask<string>(RequestById(id));
}

讓我們仔細看看這段程式碼:如果快取中有一個值,我們就會建立一個結構,否則真正的任務將被包裝在一個有意義的結構中。 呼叫程式碼並不關心此程式碼在哪個路徑中執行:從 C# 語法的角度來看,在這種情況下,ValueTask 的行為與常規任務相同。

TaskSchedulers:管理任務啟動策略

我想考慮的下一個 API 是類 任務調度 及其衍生物。 我上面已經提到,TPL 能夠管理跨執行緒分配任務的策略。 此類策略在 TaskScheduler 類別的後代中定義。 幾乎您可能需要的任何策略都可以在圖書館中找到。 並行擴展額外,由 Microsoft 開發,但不是 .NET 的一部分,而是作為 Nuget 套件提供。 讓我們簡單地看一下其中的一些:

  • 目前執行緒任務調度器 — 在目前執行緒上執行任務
  • 有限並發等級任務排程程序 — 透過參數 N 限制同時執行的任務數量,該參數在建構函數中接受
  • 有序任務調度器 — 定義為 LimitedConcurrencyLevelTask​​Scheduler(1),因此任務將依序執行。
  • 工作竊取任務調度程序 — 實現 偷工減料 任務分配方法。 本質上它是一個單獨的線程池。 解決了.NET中ThreadPool是一個靜態類,適用於所有應用程式的問題,這意味著它在程式的某一部分的重載或不正確的使用可能會導致另一部分的副作用。 而且,要了解此類缺陷的原因極為困難。 那。 在程式的某些部分中,可能需要使用單獨的 WorkStealingTaskScheduler,其中 ThreadPool 的使用可能是激進且不可預測的。
  • 佇列任務調度器 — 允許您根據優先權佇列規則執行任務
  • 執行緒每任務調度器 - 為在其上執行的每個任務建立一個單獨的執行緒。 對於需要花費不可預測的長時間才能完成的任務非常有用。

有一個很好詳細的 文章 有關 Microsoft 部落格上的 TaskSchedulers 的資訊。

為了方便偵錯與任務相關的所有內容,Visual Studio 有一個任務視窗。 在此視窗中您可以看到任務的目前狀態並跳到目前正在執行的程式碼行。

.NET:用於處理多執行緒和非同步的工具。 第1部分

PLinq 和 Parallel 類

除了任務和有關它們的所有內容之外,.NET 中還有兩個更有趣的工具:PLinq (Linq2Parallel) 和 Parallel 類別。 第一個承諾在多個執行緒上並行執行所有 Linq 操作。 可以使用 WithDegreeOfParallelism 擴充方法來配置執行緒數。 不幸的是,大多數情況下,預設模式下的PLinq 沒有足夠的關於資料來源內部的資訊來提供顯著的速度增益,另一方面,嘗試的成本非常低:您只需要在之前呼叫AsParallel 方法即可Linq 方法鏈並執行效能測試。 此外,還可以使用分區機制向 PLinq 傳遞有關資料來源性質的附加資訊。 您可以閱讀更多內容 這裡 и 這裡.

Parallel 靜態類別提供了平行迭代 Foreach 集合、執行 For 迴圈以及並行執行多個委託 Invoke 的方法。 當前執行緒的執行將停止,直到計算完成。 可以透過傳遞 ParallelOptions 作為最後一個參數來配置線程數。 您也可以使用選項指定 TaskScheduler 和 CancellationToken。

發現

當我根據報告的資料和之後工作中收集到的信息開始寫這篇文章時,我沒想到會有這麼多。 現在,當我在其中輸入本文的文字編輯器責備地告訴我第 15 頁已經消失時,我將總結中期結果。 其他技巧、API、視覺化工具和陷阱將在下一篇文章中介紹。

結論:

  • 您需要了解處理執行緒、非同步和並行的工具,才能使用現代 PC 的資源。
  • .NET 有許多不同的工具用於這些目的
  • 它們並非全部同時出現,因此您經常可以找到遺留的 API,但是,有一些方法可以輕鬆轉換舊 API。
  • 在 .NET 中使用執行緒由 Thread 和 ThreadPool 類別表示
  • Thread.Abort、Thread.Interrupt 和 Win32 API TerminateThread 方法很危險,不建議使用。 相反,最好是使用 CancellationToken 機制
  • 流量是一種寶貴的資源,但其供給是有限的。 應避免線程忙於等待事件的情況。 為此,使用 TaskCompletionSource 類別很方便
  • 用於處理平行性和非同步性的最強大、最先進的 .NET 工具是任務。
  • C# async/await 運算子實現了非阻塞等待的概念
  • 您可以使用 TaskScheduler 衍生類別來控制跨執行緒的任務分配
  • ValueTask 結構可用於優化熱路徑和記憶體流量
  • Visual Studio 的任務和執行緒視窗提供了大量對於偵錯多執行緒或非同步程式碼有用的信息
  • Plinq 是一個很酷的工具,但它可能沒有足夠的有關資料來源的信息,但這可以使用分區機制來修復
  • 待續...

來源: www.habr.com

添加評論