<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    理解C#中的ConfigureAwait

    共 17283字,需瀏覽 35分鐘

     ·

    2020-08-30 11:42

    轉(zhuǎn)自:xiaoxiaotank
    cnblogs.com/xiaoxiaotank/p/13529413.html

    前言


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


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


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


    接下來,我們會(huì)從SynchronizationContext開始講起,然后過渡到ConfigureAwait,希望這篇文章能夠?yàn)槟憬饣蟆U話少說,進(jìn)入正文。


    一、什么是SynchronizationContext


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


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


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


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


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


    例如,WinForm有一個(gè)派生自SynchronizationContext的類,重寫了Post方法,內(nèi)部執(zhí)行Control.BeginInvoke,這樣,調(diào)用該P(yáng)ost方法就會(huì)在該控件的UI線程上執(zhí)行接收的委托。


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


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


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


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


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


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


    那么,我會(huì)實(shí)現(xiàn)這樣一個(gè)自定義類:


    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();
    }
    }
    }


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


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


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


    我不需在WinForm中知道要獲取一個(gè)控件并調(diào)用BeginInvoke,不需要在WPF中知道要對(duì)Dispatcher進(jìn)行BeginInvoke,也不需要在xunit中知道要以某種方式獲取其上下文并排隊(duì),我只需要獲取當(dāng)前的SynchronizationContext并在以后使用它就可以了。


    為此,借助SynchronizationContext提供的Current屬性,我可以編寫如下代碼來實(shí)現(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方法進(jìn)行設(shè)置。


    二、什么是TaskScheduler?


    SynchronizationContext是對(duì)“調(diào)度程序(scheduler)”的通用抽象。個(gè)別框架會(huì)有自己的抽象調(diào)度程序,比如System.Threading.Tasks。當(dāng)Tasks通過委托的形式進(jìn)行排隊(duì)和執(zhí)行時(shí),會(huì)用到System.Threading.Tasks.TaskScheduler。


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


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


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


    和SynchronizationContext一樣,TaskScheduler也有一個(gè)Current屬性,會(huì)返回當(dāng)前調(diào)度程序。不過,和SynchronizationContext不同的是,它沒有設(shè)置當(dāng)前調(diào)度程序的方法,而是在啟動(dòng)Task時(shí)就要提供,因?yàn)楫?dāng)前調(diào)度程序是與當(dāng)前運(yùn)行的Task相關(guān)聯(lián)的。


    所以,下方的示例程序會(huì)輸出“True”,這是因?yàn)楹蚐tartNew一起使用的lambda表達(dá)式是在ConcurrentExclusiveSchedulerPair的ExclusiveScheduler上執(zhí)行的(我們手動(dòng)指定cesp.ExclusiveScheduler),并且TaskScheduler.Current也會(huì)指向該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提供了一個(gè)靜態(tài)的FromCurrentSynchronizationContext方法,該方法會(huì)創(chuàng)建一個(gè)SynchronizationContextTaskScheduler實(shí)例并返回,以便在原始的SynchronizationContext.Current上的Post方法對(duì)任務(wù)進(jìn)行排隊(duì)執(zhí)行。


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


    假設(shè)有一個(gè)UI App,它有一個(gè)按鈕。當(dāng)點(diǎn)擊按鈕后,會(huì)從網(wǎng)上下載一些文本并將其設(shè)置為按鈕的內(nèi)容。我們應(yīng)當(dāng)只在UI線程中訪問該按鈕,因此當(dāng)我們成功下載新的文本后,我們需要從擁有按鈕控制權(quán)的的線程中將其設(shè)置為按鈕的內(nèi)容。如果不這樣做的話,會(huì)得到一個(gè)這樣的異常:


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


    如果我們自己手動(dòng)實(shí)現(xiàn),那么可以使用前面所述的SynchronizationContext將按鈕內(nèi)容的設(shè)置傳回原始上下文,例如借助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);
    });
    }


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


    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線程上設(shè)置了按鈕的內(nèi)容,與上面手動(dòng)實(shí)現(xiàn)的版本一樣,await Task默認(rèn)會(huì)關(guān)注SynchronizationContext.Current和TaskScheduler.Current兩個(gè)參數(shù)。


    當(dāng)你在C#中使用await時(shí),編譯器會(huì)進(jìn)行代碼轉(zhuǎn)換來向“可等待者”(這里為Task)索要(通過調(diào)用GetAwaiter)“awaiter”(這里為TaskAwaiter)。


    該awaiter負(fù)責(zé)掛接回調(diào)(通常稱為“繼續(xù)(continuation)”),當(dāng)?shù)却膶?duì)象完成時(shí),該回調(diào)將被封送到狀態(tài)機(jī),并使用在注冊(cè)回調(diào)時(shí)捕獲的上下文或調(diào)度程序來執(zhí)行此回調(diào)。


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


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


    說人話就是,它先檢查有沒有設(shè)置當(dāng)前SynchronizationContext,如果沒有,則再判斷當(dāng)前調(diào)度程序是否為默認(rèn)的TaskScheduler。


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


    四、ConfigureAwait(false)做了什么?


    ConfigureAwait方法并沒有什么特別:編譯器或運(yùn)行時(shí)均不會(huì)以任何特殊方式對(duì)其進(jìn)行標(biāo)識(shí)。它僅僅是一個(gè)返回結(jié)構(gòu)體(ConfiguredTaskAwaitable)的方法,該結(jié)構(gòu)體包裝了調(diào)用它的原始任務(wù)以及調(diào)用者指定的布爾值。


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


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


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


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


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


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


    ConfigureAwait(continueOnCapturedContext: false)用于避免強(qiáng)制在原始上下文或調(diào)度程序中進(jìn)行回調(diào),有以下好處:


    提升性能


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


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


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


    避免死鎖


    假如有一個(gè)方法,使用await等待網(wǎng)絡(luò)下載結(jié)果,你需要通過同步阻塞的方式調(diào)用該方法等待其完成,比如使用.Wait()、.Result或.GetAwaiter().GetResult()。


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


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


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


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


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


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


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


    七、什么時(shí)候應(yīng)該使用ConfigureAwait(false)?


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


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


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

    這就形成了以下一般指導(dǎo):如果您正在編寫應(yīng)用程序級(jí)代碼,請(qǐng)不要使用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í)行,但如果代碼違反了該準(zhǔn)則,在錯(cuò)誤的情況下使用了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;
    }


    這將導(dǎo)致出現(xiàn)錯(cuò)誤的結(jié)果。依賴于HttpContext.Current的經(jīng)典ASP.NET應(yīng)用程序中的代碼也是如此,使用ConfigureAwait(false)然后嘗試使用HttpContext.Current也可能會(huì)導(dǎo)致問題。


    相反,通用庫之所以成為“通用庫”,原因之一是因?yàn)樗鼈儾魂P(guān)心使用它們的環(huán)境。您可以在Web應(yīng)用程序、客戶端應(yīng)用程序或測(cè)試程序中使用它們,這無關(guān)緊要,因?yàn)閹齑a與可能使用的應(yīng)用程序模型無關(guān)。


    那么,無關(guān)就意味著它不會(huì)做任何需要以特定方式與應(yīng)用程序模型進(jìn)行交互的事情,例如:它不會(huì)訪問UI控件,因?yàn)橥ㄓ脦鞂?duì)UI控件一無所知。


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


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


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


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


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


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

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


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


    即使有這些特殊情況,一般指導(dǎo)仍然是一個(gè)很好的起點(diǎn):如果要編寫通用庫或與應(yīng)用程序模型無關(guān)的代碼,請(qǐng)使用ConfigureAwait(false),否則請(qǐng)不要這樣做。


    八、以下是一些常見問題


    ConfigureAwait(false)能保證回調(diào)不會(huì)在原始上下文中運(yùn)行嗎?


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


    因?yàn)楫?dāng)?shù)却呀?jīng)完成的可等待對(duì)象時(shí)(即Task實(shí)例返回時(shí)該Task已經(jīng)完成了),后續(xù)代碼將會(huì)保持同步運(yùn)行,而無需強(qiáng)制排隊(duì)等待。


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


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


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


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


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


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


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


    是的,你可以這樣寫:


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


    沒有必要對(duì)SomethingAsync調(diào)用ConfigureAwait(false),因?yàn)閭鬟f給Task.Run的委托將運(yùn)行在線程池線程上,堆棧上沒有更高級(jí)別的用戶代碼,因此SynchronizationContext.Current將返回null。


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


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


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


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


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


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


    如果啟用了這樣的分析器,并采用該技巧來避免使用ConfigureAwait,那么分析器很有可能會(huì)標(biāo)記它,這其實(shí)會(huì)給您帶來更多工作。那么,也許您可能會(huì)因?yàn)槠錈_而禁用了分析器,這將會(huì)導(dǎo)致您忽略代碼庫中實(shí)際上應(yīng)該一直使用ConfigureAwait(false)的其他代碼。


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


    不行!額。。好吧,也許可以。這取決于你寫的代碼??赡芤恍╅_發(fā)者這樣寫:


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

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

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


    我們希望CallCodeThatUsesAwaitAsync中的代碼看到的當(dāng)前上下文是null,而且確實(shí)如此。


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


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


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


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


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


    而且即使它確實(shí)在同一線程上運(yùn)行,也可能要等一會(huì)兒,這樣一來,上下文仍無法適當(dāng)恢復(fù)。而且,如果它在其他線程上運(yùn)行,可能最終會(huì)在該線程上設(shè)置錯(cuò)誤的上下文。等等。很不理想。


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


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


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


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


    如之前的FAQ,僅僅因?yàn)槟谑褂玫膽?yīng)用程序模型未設(shè)置自定義SynchronizationContext且未在自定義TaskScheduler上調(diào)用您的代碼并不意味著其他用戶或庫代碼未設(shè)置。因此,您需要確保不存在這種情況,或至少要意識(shí)到這種風(fēng)險(xiǎn)。


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


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


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


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


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


    我在await using一個(gè)IAsyncDisposable的對(duì)象時(shí)我可以使用ConfigureAwait嗎?


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


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


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


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


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


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


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


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


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


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


    目前還沒有,至少?zèng)]有內(nèi)置在語言、編譯器或運(yùn)行時(shí)中。不過,對(duì)于這種解決方案可能是什么樣的,有許多建議,比如:


    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


    如果這對(duì)您很重要,或者您有新的有趣的想法,我鼓勵(lì)您為這些或新的討論貢獻(xiàn)自己的想法。

    往期精彩回顧




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

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

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

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

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

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

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

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

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

    關(guān)于C#異步編程你應(yīng)該了解的幾點(diǎn)建議

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

    給我好看

    您看此文用

    ??·?

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

    好看你就

    點(diǎn)點(diǎn)


    瀏覽 50
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)
    評(píng)論
    圖片
    表情
    推薦
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    国产精品美女在线 | 11久久| 黄色成人网址 | 国产精品日韩高清北条麻衣 | 天天操夜夜操xxxxxx |