【突然想多瞭解一點】可以用 Task.Run() 將同步方法包裝為非同步方法嗎?

来源:https://www.cnblogs.com/tangbunao/archive/2022/09/02/16639899.html
-Advertisement-
Play Games

【突然想多瞭解一點】可以用 Task.Run() 將同步方法包裝為非同步方法嗎? 本文翻譯自《Should I expose asynchronous wrappers for synchronous methods? - Stephen Toub》,原文地址:Should I expose asyn ...


【突然想多瞭解一點】可以用 Task.Run() 將同步方法包裝為非同步方法嗎?

本文翻譯自《Should I expose asynchronous wrappers for synchronous methods? - Stephen Toub》,原文地址:Should I expose asynchronous wrappers for synchronous methods?(microsoft.com)

註:我會對照原文進行逐句翻譯,但是考慮到中西方表達方式以及中英文語法的差異,我會適當的修改語句的順序和陳述方式。此外,限於自身英文和技術水平,有些詞或者句子的翻譯並不能表達原文的意思,對於這些詞語我會同時標註原文用詞。個人水平有限,有不對的地方請多批評指教。文章中會添加我自己對原文的一些理解,有不對的地方也請多批評指教。

 

概述

本文將會介紹 為什麼不推薦對外公開那些使用 Task.Run 將同步方法包裝為非同步的方法。

 

引言

如果各位學習過或者接觸過 C# 中基於任務的非同步編程,那麼肯定對 Task.Run() 方法不陌生。Task.Run() 方法用於線上程池中運行指定的操作Task.Run 再結合 C# 中的 asyncawait 兩個關鍵字,會讓編寫非同步代碼變的“很簡單”,就像寫同步代碼一樣。

初次嘗到非同步編程的甜頭再加上對非同步編程淺嘗輒止,就可能會想產生一個很普遍的想法:我要把原有的同步方法都包裝成非同步方法。

例如,

//原有的同步方法
public T Foo()
{
	//一些代碼
}

//設想如下
public Task<T> FooAsync()
{
	return Task.Run(() => Foo());
}

註:我就這麼乾過。

那是否推薦這種做法呢?作者 Stephen Toub 說:別這樣er

至於原因,下麵的內容都是原因。

 

1. 為什麼要非同步?

在使用一種新的技術之前我們通常會考慮一個問題,為什麼要使用這種技術,它對我的程式有幫助嗎?

在我看來非同步有兩個主要的好處:可擴展性(scalability)負載轉移(offloading,例如響應性、並行性)

那這兩個哪一個更重要呢?這個問題一般與應用程式的類型相關。大多數客戶端應用出於負載轉移的原因而關心非同步,例如要保持 UI 線程的響應性。而如果應用中有較多技術運算(technical computing,例如科學領域的數據計算)或者基於代理的模擬工作負荷(agent-based simulation workloads)時,可擴展性對客戶端應用也很重要。大多數伺服器應用(例如 ASP.NET 應用)更多的是出於可擴展性的考慮而關心非同步,當然如果需要在後端伺服器中實現並行的時候,負載轉移也重要。

以下內容是我自己加的,僅供娛樂,有不對的地方請指教批評。

 

關於 scalability 和 offloading:不太知道應該怎麼翻譯,查閱了英文釋義也沒能準確地表達出來,我做的瞭解如下:

關於 technical computing 和 agent-based simulation workload:我不太明白這兩個詞所對應的工作領域,目前理解就是有大量計算的工作。

 

2. 可擴展性(scalability)

非同步調用同步方法的方式對可擴展性沒有任何幫助,因為這種方式通常還是會消耗和同步調用這個方法時相同數量的資源(實際上,非同步調用同步方法使用的資源更多一點,因為需要有開銷安排一些事情),你只是使用不同的資源來做這件事,例如這種方式只是使用來自線程池的線程執行操作而不是當前正在執行的那個線程。

非同步帶來的可擴展性這個好處是通過減少使用的資源量來實現的,這需要從非同步方法的具體實現上來體現,這不是簡單的通過在同步方法的外部包裝一個非同步調用來實現的。

以下內容是我自己加的,僅供娛樂,有不對的地方請指教批評。

 

真正的非同步操作是很難自己去實現的,.NET 庫中提供的非同步方法都是使用”標準P/Invoke非同步I/O系統“實現的,這種真正的非同步操作不會有其它線程的參與。所以自己基於.NET中提供的同步方法包裝的非同步方法是不會有助於可擴展性的。可以參考 Stephen Cleary 的文章:There Is No Thread (stephencleary.com),這篇文章後續可能會進行翻譯,方便自己快速回顧。

舉個例子,有一個同步方法 Sleep(),該方法在 N 毫秒後才會結束執行:

public void Sleep(int millisecondsTimeout)
{
	Thread.Sleep(millisecondsTimeout);
}

接下來,需要為 Sleep() 方法創建一個非同步版本。下麵是第一種實現方式,使用 Task.Run() 方法將原有的 Sleep() 方法包裹起來:

public Task SleepAsync(int millisecondsTimeout)
{
	return Task.Run(() => Sleep(millisecondsTimeout));
}	

然後看第二種實現方式,這種實現方式沒有使用原有的 Sleep() 方法,而是重寫內部實現以消耗更少的資源:

public Task SleepAsync(int millisecondsTimeout)
{
    TaskCompletionSource<bool> tcs = null;
    var t = new Timer(delegate { tcs.TrySetResult(true); }, null, –1, -1);
    tcs = new TaskCompletionSource<bool>(t);
    t.Change(millisecondsTimeout, -1);
    return tcs.Task;
}

以上兩種非同步的實現方式都實現了相同的操作,都在指定時間後才結束任務並返回。但是,從可擴展性的角度來說,第二種方式更具有可擴展性。第一種方式在等待期間消耗了線程池中的一個線程,而第二種方式僅僅依賴於一個有效的計時器在持續時間到期後向任務發出完成的信號。

以下內容是我自己加的,僅供參考,有不對的地方請指教批評。

 

第一中方式沒有減少資源消耗,只是把阻塞的線程從調用它的線程轉到了線程池中的另一個線程,這對擴展性來說沒有提升,但它確實可以避免阻塞調用它的線程,這對 UI 應用來說是有用的,但是在非同步代碼中一般會使用 Task.Delay() 而不是 Thread.Sleep()。兩者的區別可以參考: c# - When to use Task.Delay, when to use Thread.Sleep? - Stack Overflow

第二種方式使用了 Timer 來實現相同的操作,文章中提到這可以消耗更少的資源,原因是這種方法僅依賴於一個計時器的回調。其實 Timer 也是使用了線程池中的線程,只不過所有的 Timer 實例只會使用同一個線程,而且 Task.Delay 方法內部也使用了 Timer,可以查看源碼:runtime/Task.cs at main · dotnet/runtime (github.com)

 

3. 負載轉移(offloading)

非同步調用同步方法的方式對於響應性非常有用,因為它允許將長時間運行的操作轉移到一個不同的線程中。重點不在於消耗了多少資源,而是在於消耗了哪些資源。

例如,在 Winform 應用程式中,主線程除了會執行運算操作之外還會處理 UI 消息迴圈。如果主線程上執行長時間的操作就會阻塞主線程從而導致應用程式失去響應。所以主線程相比其他線程(例如 ThreadPool 中的線程)來說,它對用戶體驗“更有價值”。所以,將方法的調用從 UI 線程轉移到 ThreadPool 的線程就能讓應用程式使用對用戶體驗來說“價值較低”的資源。這種負載轉移不需要修改原有方法的實現,它可以通過包裝原有方法來實現響應性的優勢。

 

非同步調用同步方法的方式不僅對更改線程非常有用,而且也很有助於脫離當前上下文(escaping the current context)。

例如,有時我們需要調用一些第三方的代碼,但我們不適合或者不確定是否適合這樣做。比如在調用棧的更高位置存在鎖,而我們不想在持有鎖的同時調用第三方代碼。再比如我們的代碼也可能繼續被其它用戶調用,而這些用戶並不希望我們的代碼花費很長時間。那我們就可以非同步調用第三方的代碼,而不是作為調用棧上更高層的一部分去同步調用它。

以下內容是我自己加的,僅供參考,有不對的地方請指教批評。

 

這部分沒有太明白,翻譯也就會不准確,建議可以閱讀原文。我大概理解就是通過非同步調用把某部分代碼和非同步方法外的執行環境分隔開。

 

非同步調用同步方法的方式對於並行也很重要。並行編程就是把一個問題分解成可以同時處理的子問題。

如果我們把一個問題拆分為多個子問題,然後依次處理每個子問題,那就不存在任何並行,因為整個問題會在單個線程上進行處理。相反,如果我們通過非同步調用將子問題轉移到另一個線程,那就可以同時處理多個子問題。與響應性一樣,這種負載轉移不需要修改原有方法的實現,可以通過包裝實現並行的優勢。

 

4. 上面說的一大堆和文章標題有什麼關係?

回到核心問題:是否應該為實際上是同步的方法公開一個非同步入口點? 我們在 .NET 4.5 中基於任務的非同步模式的立場下應該堅定的說:不。

請註意,上述關於可擴展性負載轉移的討論中,我們瞭解到真正實現可擴展性優勢的方法是通過修改同步方法的具體實現,而負載轉移則可以通過包裝同步方法來實現,它並不需要修改同步方法的具體實現。這就是關鍵。用簡單的非同步外觀包裝同步方法不會產生任何可擴展性優勢。而僅公開同步方法,就可以獲得一些不錯的好處,例如:

  • 庫更加簡潔。這意味著這個庫的成本更低,包括開發、測試、維護、文檔等等。這同時簡化了這個庫的用戶的選擇。雖然有更多選擇通常是一件好事,但過多的選擇往往會導致生產力下降。如果我作為用戶面對同一個操作的同步和非同步方法,我經常需要評估哪一種方法是適合我在不同情況下使用的。
  • 庫的用戶將會明白這個庫所公開的非同步的 API 是否真正具有可擴展性優勢。因為根據共識,只有真正有助於可擴展性的 API 才會以非同步方式公開。
  • 是否非同步調用同步方法的選擇由開發人員決定。圍繞同步方法的非同步包裝器具有開銷(例如,分配記憶體、上下文切換等)。例如,如果您的客戶正在編寫一個高吞吐量的伺服器應用程式,他們不想將精力花費在實際上對他們沒有任何好處的開銷上,因此他們可以調用同步方法。如果同步方法和基於它的非同步包裝方法都對公開了,那麼開發人員就可能會出於可擴展性的考慮而調用非同步版本的方法,但實際上這種簡單包裝的非同步方法不存在可擴展性的優勢,這會引起額外的開銷而有損於他們的吞吐量。

 

如果開發人員需要獲得更好的可擴展性,他們就可以使用任何公開的非同步 API,並且他們不必為調用虛假非同步 API(指用非同步包裝的同步方法) 承擔額外的開銷。而如果他們需要通過同步 API 實現響應性或並行性,他們可以簡單地使用 Task.Run 之類的方法包裝然後再調用。

 

在你的代碼庫中公開“基於同步的非同步方法(async over sync)”的這種想法是很糟糕的,極端情況下每個方法都會同時公開它的同步和非同步形式。有很多人問過我這種做法,他們想為長時間運行的 CPU 密集型的操作通過非同步包裝器公開為非同步方法,這種想法的意圖是好的:提升響應性。但就像前面所說,API 的使用者自己就可以輕鬆實現響應性,並且他們更加能知道應該在哪個層面上去做到這一點,而不需要針對每個調用進行單獨操作。另外,定義哪些操作可能是長時間運行是非常困難的,許多方法的時間複雜度通常變化很大。

以下內容是我自己加的,僅供參考,有不對的地方請指教批評。

 

基於同步的非同步方法:

這句話的原文是 “async over sync”,按照我的理解這句話是指那些使用 Task.Run 這種方法把原有的同步方法包裝成為的非同步方法。或者也可以翻譯成”同步之上的非同步“,大概意思就是這樣吧。

例如,Dictionary<TKey,TValue>.Add(TKey,TValue),這是一個非常快速的方法,但請記住 Dictonary 類是如何工作的:它需要對 Key 進行哈希處理才能找到正確的用來保存它的 bucket,並且它需要檢查該 Key 與 bucket 中已存在的其他項是否相等。這一系列哈希處理和相等性檢查可能會導致調用用戶代碼,而這些操作具體做什麼或需要多長時間是不知道的。那 Dictionary 類上的每個方法都應該公開一個非同步包裝器嗎?這顯然是一個極端的例子,但也有簡單點兒的例子,比如 Regex,提供給 Regex 的正則表達式模式的複雜性以及輸入字元串的性質和大小可能會對 Regex 匹配的運行時間產生較大影響,以至於 Regex 現在支持可選超時。Regex 上的每個方法都應該有等價的非同步方法嗎?我真的希望不會那樣。

 

5. 總結

我認為應該公開的非同步方法只有那些比它自己的同步方法更具有可擴展性優勢的方法。不應該僅僅只為了實現負載轉移的目的而公開對應的非同步方法。同步方法的調用者可以很容易地通過使用專門針對非同步處理同步方法的功能來實現這些好處,例如 Task.Run

當然,這也有例外,在 .NET 4.5 中就存在一些這樣的例外。例如,抽象基類 Stream 提供了 ReadAsyncWriteAsync 方法。在大多數情況下,Stream 的派生類使用不在記憶體中的數據源,因此派生類一般會涉及某種磁碟 I/O 或網路 I/O。而派生類很可能能夠提供利用非同步 I/O 而不是阻塞線程的同步 I/O 的 ReadAsyncWriteAsync 的實現,因此派生類的擁有的 ReadAsyncWriteAsync 方法使其具有了可擴展性優勢。

此外,我們希望能夠多態地使用這些方法,而不考慮具體的 Stream 類型,因此我們希望將這兩個方法作為基類上的虛擬方法。但是,基類不知道如何使用非同步 I/O 來完成這些方法的基本實現,因此它所能做的最好的事情是為同步的 ReadWrite 方法提供非同步包裝器(實際上,ReadAsyncWriteAsync 實際上包裝了 BeginRead/EndReadBeginWrite/EndWrite,而它們如果沒有被重寫,則將依次用等效的 Task.Run 包裝同步的 ReadWrite 方法)。

另一個類似的例子是 TextReader,它提供了 ReadToEndAsync 之類的方法,它在基類的實現中只是使用一個 Task 來包裝對 TextReader.ReadToEnd 的調用。但是,它期望開發人員實際使用的派生類會重寫 ReadToEndAsync 以提供有利於可擴展性的實現,例如使用了 Stream.ReadAsync 方法的 StreamReaderReadToEndAsync 方法。

以下內容是我自己加的,僅供參考,有不對的地方請指教批評。

 

聽君一席話,如聽一席話。

讀完整篇文章之後,可能會覺得好像看半天,但好像又沒有學到什麼。不過我還是想說一下我自己從這篇文章看到的東西。

 

在我們使用非同步的時候,首先要想清楚使用非同步的目的是什麼。如果只是因為微軟推薦使用非同步或者大家都說非同步好,所以就把原有的同步方法或者準備創建的新的同步方法通過 Task.Run 改成非同步方法,那這樣的想法是錯誤的。因為文章中已經提到,如果是為了提升性能而這麼做的話是沒有意義的,它不會提升程式性能,反而可能會引起性能問題。但是如果是為了實現類似不阻塞 Winform 主線程的效果的話也是可以這麼做的。

 

原文的標題是 Should I expose asynchronous wrappers for synchronous methods,是指如果我們寫的代碼是需要提供給其他人使用的,是否應該對外公開這種假非同步方法。當然讀完文章後我們自然明白這種做法是不應該的。

 

其次,當我們想好使用非同步的目的後,就要考慮如何實現非同步了。文章中提到自己實現一個真正的非同步是很難的,所以在自己編寫 .NET 沒有提供的非同步方法時就要慎重了。

 

最後,我翻譯文章主要是為了方便自己以後能快速回顧(畢竟看英文需要在腦子中先翻譯成中文才能開始消化),另外把自己看到的內容輸出出去也是一種吸收。英文和技術水平都有限,有不對的地方請指教批評,感謝!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 大家好,我是不才陳某~ RPC、gRPC、Thrift、HTTP,大家知道它們之間的聯繫和區別麽?這些都是面試常考的問題,今天帶大家先搞懂 RPC 和 gRPC。 在講述 gRPC 之前,我們需要先搞懂什麼是 RPC。 不 BB,直接上文章目錄: 什麼是 RPC ? RPC(Remote Proce ...
  • 一、如果是使用 Qt Designer設計界面的話,那麼如何將Qt Designer設計出來的界面(.ui 文件)與業務邏輯程式接合起來,如下兩個方法:方法一:將.ui 文件通過命令轉換成 .py文件,然後在業務邏輯代碼中進行import xxxx導入即可 1、進入 .ui文件所在目錄,然後用命令: ...
  • 蘋果發佈Xcode 7之後,可以打開正常的AppleID或實機上傳,而不是$ 99或$ 299,只要你可以在AppStore下載應用程式的AppleID。關於Mac系統和Xcode的安裝,如果不請參見原文描述,這裡只介紹使用Xcode7和普通AppID創建免費證書、個人資料。 文中測試環境是OSX ...
  • parallelStream 一定更快嗎? 大家都知道 Stream 分為順序流和並行流: stream(順序流) parallelStream(並行流) 它們最大的區別就是 parallelStream 支持並行化處理,所以效率較 stream(順序流)肯定是要更快的。這篇不會介紹 Stream ...
  • 任務編排工具 我們發現我們現在管理微服務比較麻煩,現在只是三個微服務,如果更多會更加麻煩,怎麼辦呢,下麵我們就來學習下docker編排工具 什麼是任務編排 編排是一個新的辭彙,經過閱讀才明白編排指的是容器的集群化和調度。另一類含義指的是容器管理,負責管理容器化應用和組件任務。 ​ docker毫無疑 ...
  • Python 絕對是一門易學難精的語言,打著簡單語法的旗號把我忽悠過來,最後發現它背後隱藏了許多複雜的實現。 如果不是作為 “玩具” 語言來學習,一定要看看全方位細緻講解的書《Python學習手冊第5版》。它涵蓋了 Python 的每一個角落,讓我明白了版本差異、作用域、函數式編程工具、相對導入、m ...
  • 圖片的裁剪、縮放、與加水印,是任何系統經常要用到的功能,它們現已集成到IUtility工具中,使用十分簡便。(具體代碼將在文末給出,支持.NET/.NET Framework/.NET Core) 現給出一張“原圖”,如下: (1)原圖裁剪後的效果如下: 裁剪的方法使用說明如下: PictureCu ...
  • 本文所指的 .NET 程式為 .NET6 的程式。因為 .NET 的版本更新很快,所以方式、方法也有變化,所以網上搜到的方法有些也過時了。以下是最近我實踐下來的一點心得(坑)。 上一篇說到 不安裝運行時運行 .NET 程式 後我們的程式已經只有一個 dll/exe 了,但是在 windows 上運行 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...