<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          理解C#中的ConfigureAwait

          共 17283字,需瀏覽 35分鐘

           ·

          2020-08-30 11:42

          轉自:xiaoxiaotank
          cnblogs.com/xiaoxiaotank/p/13529413.html

          前言


          七年前(原文發(fā)布于2019年).NET的編程語言和框架庫添加了async/await語法糖。自那以后,它猶如星火燎原一般,不僅遍及整個.NET生態(tài),還被許許多多的其他語言和框架所借鑒。


          當然,.NET也有很大改進,就拿對使用異步的語言結構上的補充來說,它提供了異步API支持,并對async/await的基礎架構進行了根本改進(特別是 .NET Core中性能和可分析性的提升)。


          然而,大家對ConfigureAwait的原理和使用仍然有一些困惑。


          接下來,我們會從SynchronizationContext開始講起,然后過渡到ConfigureAwait,希望這篇文章能夠為你解惑。廢話少說,進入正文。


          一、什么是SynchronizationContext


          System.Threading.SynchronizationContext的文檔是這樣說的:“提供在各種同步模型中傳播同步上下文的基本功能”,太抽象了。


          在99.9%的使用場景中,SynchronizationContext僅僅被當作一個提供虛(virtual)Post方法的類,該方法可以接收一個委托,然后異步執(zhí)行它。


          雖然SynchronizationContext還有許多其他的虛成員,但是很少使用它們,而且和我們今天的內容無關,就不說了。


          Post方法的基礎實現(xiàn)就僅僅是調用一下ThreadPool.QueueUserWorkItem,將接收的委托加入線程池隊列去異步執(zhí)行。


          另外,派生類可以選擇重寫(override)Post方法,讓委托在更加合適的位置和時間去執(zhí)行。


          例如,WinForm有一個派生自SynchronizationContext的類,重寫了Post方法,內部執(zhí)行Control.BeginInvoke,這樣,調用該Post方法就會在該控件的UI線程上執(zhí)行接收的委托。


          WinForm依賴Win32的消息處理機制,并在UI線程上運行“消息循環(huán)”,該線程就是簡單的等待新消息到達,然后去處理。這些消息可能是鼠標移動和點擊、鍵盤輸入、系統(tǒng)事件、可供調用的委托等。


          所以,只需要將委托傳遞給SynchronizationContext實例的Post方法,就可以在控件的UI線程中執(zhí)行。


          和WinForm一樣,WPF也有一個派生自SynchronizationContext的類,重寫了Post方法,通過Dispatcher.BeginInvoke將接收的委托封送到UI線程。與WinForm通過控件管理不同的是,WPF是由Dispatcher管理的。


          Windows運行時(WinRT)也不例外,它有一個派生自SynchronizationContext的類,重寫了Post方法,通過CoreDispatcher將接收的委托排隊送到UI線程。


          當然,不僅僅“在UI線程中執(zhí)行該委托”這一種用法,任何人都可以重寫SynchronizationContext的Post方法做任何事。


          例如,我可能不會關心委托在哪個線程上執(zhí)行,但是我想確保任何在我自定義的SynchronizationContext實例中執(zhí)行的任何委托都可以在一定的并發(fā)程度下執(zhí)行。


          那么,我會實現(xiàn)這樣一個自定義類:


          internal sealed class MaxConcurrencySynchronizationContext : SynchronizationContext
          {
          private readonly SemaphoreSlim _semaphore;
          public MaxConcurrencySynchronizationContext(int maxConcurrencyLevel) =>
          _semaphore = new SemaphoreSlim(maxConcurrencyLevel);
          public override void Post(SendOrPostCallback d, object state) =>
          _semaphore.WaitAsync().ContinueWith(delegate
          {
          try
          {
          d(state);
          }
          finally
          {
          _semaphore.Release();
          }
          }, default, TaskContinuationOptions.None, TaskScheduler.Default);
          public override void Send(SendOrPostCallback d, object state)
          {
          _semaphore.Wait();
          try
          {
          d(state);
          }
          finally
          {
          _semaphore.Release();
          }
          }
          }


          事實上,單元測試框架xunit就提供了一個SynchronizationContext的派生類,和我寫的這個很類似,用于限制可以并發(fā)的測試相關的代碼量。


          與抽象的優(yōu)點一樣:它提供了一個API,可用于將委托排隊進行處理,無需了解該實現(xiàn)的細節(jié),這是實現(xiàn)者所期望的。


          所以,如果我正在編寫一個庫,想要停下來做一些工作,然后將委托排隊送回“原始上下文”繼續(xù)執(zhí)行,那么我只需要獲取他們的SynchronizationContext,存下來。當完成工作后,在該上下文上調用Post去傳遞我想要調用的委托即可。


          我不需在WinForm中知道要獲取一個控件并調用BeginInvoke,不需要在WPF中知道要對Dispatcher進行BeginInvoke,也不需要在xunit中知道要以某種方式獲取其上下文并排隊,我只需要獲取當前的SynchronizationContext并在以后使用它就可以了。


          為此,借助SynchronizationContext提供的Current屬性,我可以編寫如下代碼來實現(xiàn)上述功能:


          public void DoWork(Action worker, Action completion)
          {
          SynchronizationContext sc = SynchronizationContext.Current;
          ThreadPool.QueueUserWorkItem(_ =>
          {
          try
          {
          worker();
          }
          finally
          {
          sc.Post(_ => completion(), null);
          }
          });
          }


          如果框架想要通過Current公開自定義的上下文,可以使用SynchronizationContext.SetSynchronizationContext方法進行設置。


          二、什么是TaskScheduler?


          SynchronizationContext是對“調度程序(scheduler)”的通用抽象。個別框架會有自己的抽象調度程序,比如System.Threading.Tasks。當Tasks通過委托的形式進行排隊和執(zhí)行時,會用到System.Threading.Tasks.TaskScheduler。


          和SynchronizationContext提供了一個virtual Post方法用于將委托排隊調用一樣(稍后,我們會通過典型的委托調用機制來調用委托),TaskScheduler也提供了一個abstract QueueTask方法(稍后,我們會通過ExecuteTask方法來調用該Task)。


          通過TaskScheduler.Default我們可以獲取到Task默認的調度程序ThreadPoolTaskScheduler——線程池(譯注:這下知道為什么Task默認使用的是線程池線程了吧)。并且可以通過繼承TaskScheduler來重寫相關方法來實現(xiàn)在任意時間任意地點進行Task調用。


          例如,核心庫中有個類,名為System.Threading.Tasks.ConcurrentExclusiveSchedulerPair,其實例公開了兩個TaskScheduler屬性,一個叫ExclusiveScheduler,另一個叫ConcurrentScheduler。調度給ConcurrentScheduler的任務可以并發(fā),但是要在構造ConcurrentExclusiveSchedulerPair時就要指定最大并發(fā)數(shù)(類似于前面演示的MaxConcurrencySynchronizationContext);相反,在ExclusiveScheduler執(zhí)行任務時,那么將只允許運行一個排他任務,這個行為很像讀寫鎖。


          和SynchronizationContext一樣,TaskScheduler也有一個Current屬性,會返回當前調度程序。不過,和SynchronizationContext不同的是,它沒有設置當前調度程序的方法,而是在啟動Task時就要提供,因為當前調度程序是與當前運行的Task相關聯(lián)的。


          所以,下方的示例程序會輸出“True”,這是因為和StartNew一起使用的lambda表達式是在ConcurrentExclusiveSchedulerPair的ExclusiveScheduler上執(zhí)行的(我們手動指定cesp.ExclusiveScheduler),并且TaskScheduler.Current也會指向該ExclusiveScheduler:


          using System;
          using System.Threading.Tasks;
          class Program
          {
          static void Main()
          {
          var cesp = new ConcurrentExclusiveSchedulerPair();
          Task.Factory.StartNew(() =>
          {
          Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler);
          }, default, TaskCreationOptions.None, cesp.ExclusiveScheduler)
          .Wait();
          }
          }


          有趣的是,TaskScheduler提供了一個靜態(tài)的FromCurrentSynchronizationContext方法,該方法會創(chuàng)建一個SynchronizationContextTaskScheduler實例并返回,以便在原始的SynchronizationContext.Current上的Post方法對任務進行排隊執(zhí)行。


          三、SynchronizationContext和TaskScheduler是如何與await關聯(lián)起來的呢?


          假設有一個UI App,它有一個按鈕。當點擊按鈕后,會從網上下載一些文本并將其設置為按鈕的內容。我們應當只在UI線程中訪問該按鈕,因此當我們成功下載新的文本后,我們需要從擁有按鈕控制權的的線程中將其設置為按鈕的內容。如果不這樣做的話,會得到一個這樣的異常:


          System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'


          如果我們自己手動實現(xiàn),那么可以使用前面所述的SynchronizationContext將按鈕內容的設置傳回原始上下文,例如借助TaskScheduler:


          private static readonly HttpClient s_httpClient = new HttpClient();
          private void downloadBtn_Click(object sender, RoutedEventArgs e)
          {
          s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
          {
          downloadBtn.Content = downloadTask.Result;
          }, TaskScheduler.FromCurrentSynchronizationContext());
          }


          或直接使用SynchronizationContext:


          private static readonly HttpClient s_httpClient = new HttpClient();
          private void downloadBtn_Click(object sender, RoutedEventArgs e)
          {
          SynchronizationContext sc = SynchronizationContext.Current;
          s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
          {
          sc.Post(delegate
          {
          downloadBtn.Content = downloadTask.Result;
          }, null);
          });
          }


          不過,這兩種方式都需要顯式指定回調,更好的方式是通過async/await自然地進行編碼:


          private static readonly HttpClient s_httpClient = new HttpClient();
          private async void downloadBtn_Click(object sender, RoutedEventArgs e)
          {
          string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
          downloadBtn.Content = text;
          }


          就這樣,成功在UI線程上設置了按鈕的內容,與上面手動實現(xiàn)的版本一樣,await Task默認會關注SynchronizationContext.Current和TaskScheduler.Current兩個參數(shù)。


          當你在C#中使用await時,編譯器會進行代碼轉換來向“可等待者”(這里為Task)索要(通過調用GetAwaiter)“awaiter”(這里為TaskAwaiter)。


          該awaiter負責掛接回調(通常稱為“繼續(xù)(continuation)”),當?shù)却膶ο笸瓿蓵r,該回調將被封送到狀態(tài)機,并使用在注冊回調時捕獲的上下文或調度程序來執(zhí)行此回調。


          盡管與實際代碼不完全相同(實際代碼還進行了其他優(yōu)化和調整),但大體上是這樣的:


          object scheduler = SynchronizationContext.Current;
          if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
          {
          scheduler = TaskScheduler.Current;
          }


          說人話就是,它先檢查有沒有設置當前SynchronizationContext,如果沒有,則再判斷當前調度程序是否為默認的TaskScheduler。


          如果不是,那么當準備好調用回調時,會使用該調度程序執(zhí)行回調;否則,通常會作為完成已等待任務的操作的一部分來執(zhí)行回調(譯注:這個“否則”我也沒看懂,我的理解是如果有當前上下文,則使用當前上下文執(zhí)行回調;如果當前上下文為空,且使用的是默認調度程序ThreadPoolTaskScheduler,則會啟用線程池線程執(zhí)行回調)。


          四、ConfigureAwait(false)做了什么?


          ConfigureAwait方法并沒有什么特別:編譯器或運行時均不會以任何特殊方式對其進行標識。它僅僅是一個返回結構體(ConfiguredTaskAwaitable)的方法,該結構體包裝了調用它的原始任務以及調用者指定的布爾值。


          注意,await可以用于任何正確模式的類型(而不僅僅是Task,在C#中只要類包含GetAwaiter() 方法和bool IsCompleted屬性,并且GetAwaiter()的返回值包含 GetResult()方法、bool IsCompleted屬性和實現(xiàn)了 INotifyCompletion接口,那么這個類的實例就是可以await 的)。


          當編譯器訪問實例的GetAwaiter方法(模式的一部分)時,它是根據(jù)ConfigureAwait返回的類型進行操作的,而不是直接使用Task,此外,還提供了一個鉤子,用于通過該自定義awaiter更改await的行為。


          具體來說,如果等待ConfigureAwait(continueOnCapturedContext:false)返回的類型ConfiguredTaskAwaitable,而非直接等待Task,最終會影響上面展示的捕獲目標上下文或調度程序的邏輯。它使得上面展示的邏輯變成了這樣:


          object scheduler = null;
          if (continueOnCapturedContext)
          {
          scheduler = SynchronizationContext.Current;
          if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
          {
          scheduler = TaskScheduler.Current;
          }
          }


          換句話說,通過指定參數(shù)為false,即使有當前上下文或調度程序用于回調,它也會假裝沒有。


          五、我為什么要使用ConfigureAwait(false)?


          ConfigureAwait(continueOnCapturedContext: false)用于避免強制在原始上下文或調度程序中進行回調,有以下好處:


          提升性能


          比起直接調用,排隊進行回調會更加耗費性能,一個是因為會有一些額外的工作(一般是額外的內存分配),另一個是因為無法使用我們本來希望在運行時中采用的某些優(yōu)化(當我們確切知道回調將如何調用時,我們可以進行更多優(yōu)化,但如果將其移交給抽象的任意實現(xiàn),則有時會受到限制)。


          對于大多數(shù)情況,即使檢查當前的SynchronizationContext和TaskScheduler也可能會增加一定的開銷(兩者都會訪問線程靜態(tài)變量)。


          如果await之后的代碼并不需要在原始上下文中運行,那么使用ConfigureAwait(false)就可以避免上述花銷:它不用排隊,且可以利用所有可以進行的優(yōu)化,還可以避免不必要的線程靜態(tài)訪問。


          避免死鎖


          假如有一個方法,使用await等待網絡下載結果,你需要通過同步阻塞的方式調用該方法等待其完成,比如使用.Wait()、.Result或.GetAwaiter().GetResult()。


          思考一下,如果限制當前SynchronizationContext并發(fā)數(shù)為1,會發(fā)生什么情況?方式不限,無論是顯式地通過類似于前面所說的MaxConcurrencySynchronizationContext的方式,還是隱式地通過僅具有一個可以使用的線程的上下文來實現(xiàn),例如UI線程,你都可以在那個線程上調用該方法并阻塞它等待操作完成,該操作將開啟網絡下載并等待。在默認情況下, 等待Task會捕獲當前SynchronizationContext,所以,當網絡下載完成時,它會將回調排隊返回到SynchronizationContext中執(zhí)行剩下的操作。但是,當前唯一可以處理排隊回調的線程卻還被你阻塞著等待操作完成,不幸的是,在回調處理完畢之前,該操作永遠不會完成。完蛋,死鎖了!


          即使不將上下文并發(fā)數(shù)限制為1,而是通過其他任何方式對資源進行了限制,結果也是如此。比如,我們將MaxConcurrencySynchronizationContext限制為4,這時,我們對該上下文進行4次排隊調用,每個調用都會進行阻塞等待操作完成?,F(xiàn)在,我們在等待異步方法完成時仍阻塞了所有資源,這些異步方法能否完成取決于是否可以在已經完全消耗掉的上下文中處理它們的回調。哦吼,又死鎖了!


          如果該方法改為使用ConfigureAwait(false),那么它就不會將回調排隊送回原始上下文,進而避免了死鎖。


          六、我為什么要使用ConfigureAwait(true)?


          絕對沒必要使用,除非你閑的蛋疼使用它來表明你是故意不使用ConfigureAwait(false)的(例如消除VS的靜態(tài)分析警告或類似的警告等),使用ConfigureAwait(true)沒有任何意義。


          await task和await task.ConfigureAwait(true)在功能上沒有任何區(qū)別,如果你在生產環(huán)境的代碼中發(fā)現(xiàn)了ConfigureAwait(true),那么你可以直接刪除它,不會有任何副作用。


          ConfigureAwait方法接收一個布爾值參數(shù),可能在某些特殊情況下,你需要通過傳入變量來控制配置,不過,99%的情況下都是通過硬編碼的方式傳入的,如ConfigureAwait(false)


          七、什么時候應該使用ConfigureAwait(false)?


          這取決于:你在實現(xiàn)應用程序級代碼還是通用庫代碼?


          當你編寫應用程序時,你通常需要使用默認行為(這就是ConfigureAwait(true)是默認行為的原因(譯注:原作者應該是想要表達編寫應用程序比通用庫更加頻繁,所以該行為會更頻繁的使用))。如果應用模型或環(huán)境(例如WinForm,WPF,ASP.NET Core等)發(fā)布了自定義SynchronizationContext,那么基本上可以肯定有一個很好的理由:它為關注同步上下文的代碼提供了一種與應用模型或環(huán)境適當交互的方式。


          所以如果你使用WinForm寫事件處理器、在xunit中寫單元測試或在ASP .NET MVC控制器中編碼,無論應用程序模型是否確實發(fā)布了SynchronizationContext,您都想使用該SynchronizationContext(如果存在),那么您可以簡單地await默認的ConfigureAwait(true),如果存在回調,就可以將其正確地封送到原始上下文中執(zhí)行。

          這就形成了以下一般指導:如果您正在編寫應用程序級代碼,請不要使用ConfigureAwait(false)。如果您回想一下本文前面的Click事件處理程序代碼示例:


          private static readonly HttpClient s_httpClient = new HttpClient();
          private async void downloadBtn_Click(object sender, RoutedEventArgs e)
          {
          string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
          downloadBtn.Content = text;
          }


          代碼downloadBtn.Content = text需要在原始上下文中執(zhí)行,但如果代碼違反了該準則,在錯誤的情況下使用了ConfigureAwait(false):


          private static readonly HttpClient s_httpClient = new HttpClient();
          private async void downloadBtn_Click(object sender, RoutedEventArgs e)
          {
          string text = await s_httpClient.GetStringAsync("http://example.com/currenttime")
          .ConfigureAwait(false); // bug
          downloadBtn.Content = text;
          }


          這將導致出現(xiàn)錯誤的結果。依賴于HttpContext.Current的經典ASP.NET應用程序中的代碼也是如此,使用ConfigureAwait(false)然后嘗試使用HttpContext.Current也可能會導致問題。


          相反,通用庫之所以成為“通用庫”,原因之一是因為它們不關心使用它們的環(huán)境。您可以在Web應用程序、客戶端應用程序或測試程序中使用它們,這無關緊要,因為庫代碼與可能使用的應用程序模型無關。


          那么,無關就意味著它不會做任何需要以特定方式與應用程序模型進行交互的事情,例如:它不會訪問UI控件,因為通用庫對UI控件一無所知。


          由于我們不需要在任何特定環(huán)境中運行代碼,那么我們可以避免將回調強制送回到原始上下文,這可以通過使用ConfigureAwait(false)來實現(xiàn),并享受到其帶來的性能和可靠性優(yōu)勢。


          這形成了以下一般指導:如果要編寫通用庫代碼,請使用ConfigureAwait(false)。


          這就是為什么您會在.NET Core運行時庫中看到每個(或幾乎每個)await時都要使用ConfigureAwait(false)的原因;如果不是這樣的話(除了少數(shù)例外),那很可能是一個要修復的BUG。例如,此Pull request修復了HttpClient中缺少的ConfigureAwait(false)調用。


          當然,與其他指導一樣,在某些特殊的情況下可能不適用。例如,在通用庫中,具有可調用委托的API是一個較大的例外(或至少需要考慮的例外)。


          在這種情況下,庫的調用者可能會傳遞由庫調用的應用程序級代碼,然后有效地呈現(xiàn)了庫那些“通用”假設。


          例如,以LINQ中Where的異步版本(運行時庫不存在該方法,僅僅是假設)為例:

          public static async IAsyncEnumerable WhereAsync(this IAsyncEnumerable source, Funcbool> predicate)。


          這里的predicate是否需要在調用者的原始SynchronizationContext上重新調用?這要取決于WhereAsync的實現(xiàn),因此,它可能選擇不使用ConfigureAwait(false)。


          即使有這些特殊情況,一般指導仍然是一個很好的起點:如果要編寫通用庫或與應用程序模型無關的代碼,請使用ConfigureAwait(false),否則請不要這樣做。


          八、以下是一些常見問題


          ConfigureAwait(false)能保證回調不會在原始上下文中運行嗎?


          并不能保證!它雖能保證它不會被排隊回到原始上下文中……但這并不意味著await task.ConfigureAwait(false)后的代碼仍不會在原始上下文中運行。


          因為當?shù)却呀浲瓿傻目傻却龑ο髸r(即Task實例返回時該Task已經完成了),后續(xù)代碼將會保持同步運行,而無需強制排隊等待。


          所以,如果您等待的任務在等待時就已經完成了,那么無論您是否使用了ConfigureAwait(false),緊隨其后的代碼也會在擁有當前上下文的當前線程上繼續(xù)執(zhí)行。


          我的方法中僅在第一次await時使用ConfigureAwait(false)而剩下的代碼不使用可以嗎?


          一般來說,不行,參考前面的FAQ。如果await task.ConfigureAwait(false)在等待時就已完成了(實際上很常見),那么ConfigureAwait(false)將毫無意義,因為線程在此之后繼續(xù)在該方法中執(zhí)行代碼,并且仍在與之前相同的上下文中執(zhí)行。


          有一個例外是:如果您知道第一次等待始終會異步完成,并且正在等待的事物會在沒有自定義SynchronizationContext或TaskScheduler的環(huán)境中調用其回調。


          例如,.NET運行時庫中的CryptoStream希望確保其潛在的計算密集型代碼不會被調用者以同步方式進行調用,因此它使用自定義的awaiter來確保第一次等待后的所有內容都在線程池線程上運行。


          但是,即使在這種情況下,您也會注意到下一次等待仍將使用ConfigureAwait(false);從技術上講,使用ConfigureAwait(false)不是必需的,但是它使代碼審查變得很容易,這樣每次查看該塊代碼時,就無需分析一番來了解為什么取消ConfigureAwait(false)。


          我可以使用Task.Run來避免使用ConfigureAwait(false)嗎?


          是的,你可以這樣寫:


          Task.Run(async delegate
          {
          await SomethingAsync(); // 不會找到原始上下文
          });


          沒有必要對SomethingAsync調用ConfigureAwait(false),因為傳遞給Task.Run的委托將運行在線程池線程上,堆棧上沒有更高級別的用戶代碼,因此SynchronizationContext.Current將返回null。


          此外,Task.Run隱式使用TaskScheduler.Default,所以TaskScheduler.Current也會指向該Default。也就是說,無論是否使用ConfigureAwait(false),await都會做出相同的行為。它也不能保證此Lambda內的代碼可以做什么。如果您寫了這樣一段代碼:


          Task.Run(async delegate
          {
          SynchronizationContext.SetSynchronizationContext(new SomeCoolSyncCtx());
          await SomethingAsync(); // will target SomeCoolSyncCtx
          });


          那么在SomethingAsync內部你會發(fā)現(xiàn)SynchronizationContext.Current就是SomeCoolSyncCtx實例,并且該await和SomethingAsync內部的所有未配置的await都將返回到該上下文。


          因此,要使用這種方式,您需要了解排隊的所有代碼可能會做什么或不做什么,以及它的行為是否會阻礙您的行為。


          這種方法還需要以創(chuàng)建或排隊其他任務對象為代價。這取決于您的性能敏感性,對您的應用程序或庫而言可能無關緊要。


          另外要注意,這些技巧可能會引起更多的問題,并帶來其他意想不到的后果。例如,靜態(tài)分析工具(例如Roslyn分析儀)提供了標記不使用ConfigureAwait(false)的標志等待,正如CA2007。


          如果啟用了這樣的分析器,并采用該技巧來避免使用ConfigureAwait,那么分析器很有可能會標記它,這其實會給您帶來更多工作。那么,也許您可能會因為其煩擾而禁用了分析器,這將會導致您忽略代碼庫中實際上應該一直使用ConfigureAwait(false)的其他代碼。


          我能用SynchronizationContext.SetSynchronizationContext來避免使用ConfigureAwait(false)嗎?


          不行!額。。好吧,也許可以。這取決于你寫的代碼。可能一些開發(fā)者這樣寫:


          Task t;
          var old = SynchronizationContext.Current;
          SynchronizationContext.SetSynchronizationContext(null);
          try
          {

          t = CallCodeThatUsesAwaitAsync(); // 在方法內部進行 await 不會感知到原始上下文

          }
          finally
          {
          SynchronizationContext.SetSynchronizationContext(old);
          }
          await t; // 這時則會回到原始上下文


          我們希望CallCodeThatUsesAwaitAsync中的代碼看到的當前上下文是null,而且確實如此。


          但是,以上內容不會影響TaskScheduler的等待狀態(tài),因此,如果此代碼在某些自定義TaskScheduler上運行,那么在CallCodeThatUsesAwaitAsync(不使用ConfigureAwait(false))內部等待后仍將排隊返回該自定義TaskScheduler。


          所有這些注意事項也適用于前面Task.Run相關的FAQ:這種解決方法可能會帶來一些性能方面的問題,并且try中的代碼也可以通過設置其他上下文(或使用非默認TaskScheduler來調用代碼)來阻止這種嘗試。


          使用這種模式,您還需要注意一些細微的變化:


          var old = SynchronizationContext.Current;
          SynchronizationContext.SetSynchronizationContext(null);
          try
          {
          await t;
          }
          finally
          {
          SynchronizationContext.SetSynchronizationContext(old);
          }


          找到問題沒?可能很難發(fā)現(xiàn)但是影響很大。這樣寫沒法保證await最終會回到原始線程上執(zhí)行回調并繼續(xù)執(zhí)行生下的代碼,也就是說將SynchronizationContext重置回原始上下文這個操作可能實際上并未在原始線程上進行,這可能導致該線程上的后續(xù)工作項看到錯誤的上下文(為解決這一問題,具有良好編碼規(guī)范的應用模型在設置了自定義上下文時,通常會在調用任何其他用戶代碼之前添加代碼以手動將其重置)。


          而且即使它確實在同一線程上運行,也可能要等一會兒,這樣一來,上下文仍無法適當恢復。而且,如果它在其他線程上運行,可能最終會在該線程上設置錯誤的上下文。等等。很不理想。


          如果我用了GetAwaiter().GetResult(),我還需要使用ConfigureAwait(false)嗎?


          不需要,ConfigureAwait只影響回調。具體來說,awaiter模式要求awaiters 公開IsCompleted屬性、GetResult方法和OnCompleted方法(可選使用UnsafeOnCompleted方法)。


          ConfigureAwait只會影響OnCompleted/UnsafeOnCompleted的行為,因此,如果您只是直接調用等待者的GetResult()方法,那么你無論是在TaskAwaiter上還是在ConfiguredTaskAwaitable.ConfiguredTaskAwaiter上進行操作,都是沒有任何區(qū)別的。因此,如果在代碼中看到task.ConfigureAwait(false).GetAwaiter().GetResult(),則可以將其替換為task.GetAwaiter().GetResult()(并考慮是否真的需要這樣的阻塞)。


          我可以跳過使用ConfigureAwait(false)嗎?也許可以,這取決于你是如何確定“永遠不會”的。


          如之前的FAQ,僅僅因為您正在使用的應用程序模型未設置自定義SynchronizationContext且未在自定義TaskScheduler上調用您的代碼并不意味著其他用戶或庫代碼未設置。因此,您需要確保不存在這種情況,或至少要意識到這種風險。


          我聽說在 .NET Core中ConfigureAwait(false)已經不再需要了,這是真的嗎?


          假的!在.NET Core上運行時仍需要使用它,和在.NET Framework上運行時需要使用的原因完全相同,在這方面沒有任何改變。


          不過,有一些變化的是某些環(huán)境是否發(fā)布了自己的SynchronizationContext。特別是雖然在.NET Framework上的經典ASP.NET具有自己的SynchronizationContext,但是ASP.NET Core卻沒有。這意味著默認情況下,在ASP.NET Core應用程序中運行的代碼是看不到自定義SynchronizationContext的,從而減少了在這種環(huán)境中運行ConfigureAwait(false)的需要。


          但這并不意味著永遠不會存在自定義的SynchronizationContext或TaskScheduler。如果某些用戶代碼(或您的應用程序正在使用的其他庫代碼)設置了自定義上下文并調用了您的代碼,或在自定義TaskScheduler的預定Task中調用您的代碼,那么即使在ASP.NET Core中,您的等待對象也可能會看到非默認上下文或調度程序,從而促使您想要使用ConfigureAwait(false)。


          當然,在這種情況下,如果您想要避免同步阻塞(任何情況下,都應避免在Web應用程序中進行同步阻塞),并且不介意在這種有限的情況下有細微的性能開銷,那您可能無需使用ConfigureAwait(false)就可以實現(xiàn)。


          我在await using一個IAsyncDisposable的對象時我可以使用ConfigureAwait嗎?


          可以,不過有些小問題。與前面的FAQ中所述的IAsyncEnumerable一樣,.NET運行時公開了一個IAsyncDisposable的擴展方法ConfigureAwait的擴展方法,并且await using能很好地與此一起工作,因為它實現(xiàn)了適當?shù)哪J剑垂_了適當?shù)腄isposeAsync方法):


          await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false))
          {
          ...
          }


          這里的問題是,變量c的類型現(xiàn)在不是MyAsyncDisposableClass,而是System.Runtime.CompilerServices.ConfiguredAsyncDisposable,這是從IAsyncDisposable上的ConfigureAwait擴展方法返回的類型。


          為了解決這個問題,您需要多寫一行:


          var c = new MyAsyncDisposableClass();
          await using (c.ConfigureAwait(false))
          {
          ...
          }


          現(xiàn)在,變量c的類型又是所需的MyAsyncDisposableClass了。這還具有增加c范圍的作用;如果有影響,則可以將整個內容括在大括號中。


          不,這是預期的。AsyncLocal數(shù)據(jù)流是ExecutionContext的一部分,它與SynchronizationContext是相互獨立的。


          除非您使用ExecutionContext.SuppressFlow()明確禁用了ExecutionContext流,否則ExecutionContext(以及AsyncLocal數(shù)據(jù))將始終在等待狀態(tài)中流動,無論是否使用ConfigureAwait來避免捕獲原始的SynchronizationContext。有關更多信息,請參見此博客。


          可以在語言層面幫助我避免在我的庫中顯式使用ConfigureAwait(false)嗎?


          類庫開發(fā)人員有時會對需要使用ConfigureAwait(false)而感到沮喪,并想要使用侵入性較小的替代方法。


          目前還沒有,至少沒有內置在語言、編譯器或運行時中。不過,對于這種解決方案可能是什么樣的,有許多建議,比如:


          https://github.com/dotnet/csharplang/issues/645


          https://github.com/dotnet/csharplang/issues/2542


          https://github.com/dotnet/csharplang/issues/2649


          https://github.com/dotnet/csharplang/issues/2746


          如果這對您很重要,或者您有新的有趣的想法,我鼓勵您為這些或新的討論貢獻自己的想法。

          往期精彩回顧




          【推薦】.NET Core開發(fā)實戰(zhàn)視頻課程?★★★

          .NET Core實戰(zhàn)項目之CMS 第一章 入門篇-開篇及總體規(guī)劃

          【.NET Core微服務實戰(zhàn)-統(tǒng)一身份認證】開篇及目錄索引

          Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)

          .NET Core中的一個接口多種實現(xiàn)的依賴注入與動態(tài)選擇看這篇就夠了

          10個小技巧助您寫出高性能的ASP.NET Core代碼

          用abp vNext快速開發(fā)Quartz.NET定時任務管理界面

          在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務輕松實現(xiàn)作業(yè)調度

          現(xiàn)身說法:實際業(yè)務出發(fā)分析百億數(shù)據(jù)量下的多表查詢優(yōu)化

          關于C#異步編程你應該了解的幾點建議

          C#異步編程看這篇就夠了

          給我好看

          您看此文用

          ??·?

          秒,轉發(fā)只需1秒呦~

          好看你就

          點點


          瀏覽 50
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩一区二区三区四区 | AV12在线| 激情小说五月天 | 欧美成人a v | 天堂草原电视剧图片在线播放 |