理解C#中的ConfigureAwait
阿新 • • 發佈:2020-08-20
> 原文:https://devblogs.microsoft.com/dotnet/configureawait-faq/
作者:Stephen
翻譯:xiaoxiaotank
靜下心來,你一定會有收穫。
七年前(原文釋出於2019年).NET的程式語言和框架庫添加了`async/await`語法糖。自那以後,它猶如星火燎原一般,不僅遍及整個.NET生態,還被許許多多的其他語言和框架所借鑑。當然,.NET也有很大改進,就拿對使用非同步的語言結構上的補充來說,它提供了非同步API支援,並對`async/await`的基礎架構進行了根本改進(特別是 .NET Core中效能和可分析性的提升)。
然而,大家對`ConfigureAwait`的原理和使用仍然有一些困惑。接下來,我們會從`SynchronizationContext`開始講起,然後過渡到`ConfigureAwait`,希望這篇文章能夠為你解惑。廢話少說,進入正文。
#### 什麼是SynchronizationContext?
[System.Threading.SynchronizationContext](https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.synchronizationcontext?view=netcore-3.1)的文件是這樣說的:“提供在各種同步模型中傳播同步上下文的基本功能”,太抽象了。
在99.9%的使用場景中,`SynchronizationContext`僅僅被當作一個提供虛(virtual)`Post`方法的類,該方法可以接收一個委託,然後非同步執行它。雖然`SynchronizationContext`還有許多其他的虛成員,但是很少使用它們,而且和我們今天的內容無關,就不說了。`Post`方法的基礎實現就僅僅是呼叫一下`ThreadPool.QueueUserWorkItem`,將接收的委託加入執行緒池佇列去非同步執行。
另外,派生類可以選擇重寫(override)`Post`方法,讓委託在更加合適的位置和時間去執行。
例如,WinForm有一個[派生自SynchronizationContext的類](https://github.com/dotnet/winforms/blob/94ce4a2e52bf5d0d07d3d067297d60c8a17dc6b4/src/System.Windows.Forms/src/System/Windows/Forms/WindowsFormsSynchronizationContext.cs),重寫了`Post`方法,內部執行`Control.BeginInvoke`,這樣,呼叫該`Post`方法就會在該控制元件的UI執行緒上執行接收的委託。WinForm依賴Win32的訊息處理機制,並在UI執行緒上執行“訊息迴圈”,該執行緒就是簡單的等待新訊息到達,然後去處理。這些訊息可能是滑鼠移動和點選、鍵盤輸入、系統事件、可供呼叫的委託等。所以,只需要將委託傳遞給`SynchronizationContext`例項的`Post`方法,就可以在控制元件的UI執行緒中執行。
和WinForm一樣,WPF也有一個[派生自SynchronizationContext的類](https://github.com/dotnet/wpf/blob/ac9d1b7a6b0ee7c44fd2875a1174b820b3940619/src/Microsoft.DotNet.Wpf/src/WindowsBase/System/Windows/Threading/DispatcherSynchronizationContext.cs),重寫了`Post`方法,通過`Dispatcher.BeginInvoke`將接收的委託封送到UI執行緒。與WinForm通過控制元件管理不同的是,WPF是由Dispatcher管理的。
Windows執行時(WinRT)也不例外,它有一個[派生自SynchronizationContext的類](https://github.com/dotnet/runtime/blob/60d1224ddd68d8ac0320f439bb60ac1f0e9cdb27/src/libraries/System.Runtime.WindowsRuntime/src/System/Threading/WindowsRuntimeSynchronizationContext.cs),重寫了`Post`方法,通過`CoreDispatcher`將接收的委託排隊送到UI執行緒。
當然,不僅僅“在UI執行緒中執行該委託”這一種用法,任何人都可以重寫`SynchronizationContext`的`Post`方法做任何事。例如,我可能不會關心委託在哪個執行緒上執行,但是我想確保任何在我自定義的`SynchronizationContext`例項中執行的任何委託都可以在一定的併發程度下執行。那麼,我會實現這樣一個自定義類:
```csharp
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的派生類](https://github.com/xunit/xunit/blob/d81613bf752bb4b8774e9d4e77b2b62133b0d333/src/xunit.execution/Sdk/MaxConcurrencySyncContext.cs),和我寫的這個很類似,用於限制可以併發的測試相關的程式碼量。
與抽象的優點一樣:它提供了一個API,可用於將委託排隊進行處理,無需瞭解該實現的細節,這是實現者所期望的。所以,如果我正在編寫一個庫,想要停下來做一些工作,然後將委託排隊送回“原始上下文”繼續執行,那麼我只需要獲取他們的`SynchronizationContext`,存下來。當完成工作後,在該上下文上呼叫`Post`去傳遞我想要呼叫的委託即可。我不需在WinForm中知道要獲取一個控制元件並呼叫`BeginInvoke`,不需要在WPF中知道要對`Dispatcher`進行`BeginInvoke`,也不需要在xunit中知道要以某種方式獲取其上下文並排隊,我只需要獲取當前的`SynchronizationContext`並在以後使用它就可以了。為此,藉助`SynchronizationContext`提供的`Current`屬性,我可以編寫如下程式碼來實現上述功能:
```
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通過委託的形式進行排隊和執行時,會用到`System.Threading.Tasks.TaskScheduler`。和`SynchronizationContext`提供了一個`virtual Post`方法用於將委託排隊呼叫一樣(稍後,我們會通過典型的委託呼叫機制來呼叫委託),`TaskScheduler`也提供了一個`abstract QueueTask`方法(稍後,我們會通過`ExecuteTask`方法來呼叫該Task)。
通過`TaskScheduler.Default`我們可以獲取到`Task`預設的排程程式`ThreadPoolTaskScheduler`——執行緒池(譯註:這下知道為什麼`Task`預設使用的是執行緒池執行緒了吧)。並且可以通過繼承`TaskScheduler`來重寫相關方法來實現在任意時間任意地點進行Task呼叫。例如,核心庫中有個類,名為`System.Threading.Tasks.ConcurrentExclusiveSchedulerPair`,其例項公開了兩個`TaskScheduler`屬性,一個叫`ExclusiveScheduler`,另一個叫`ConcurrentScheduler`。排程給`ConcurrentScheduler`的任務可以併發,但是要在構造`ConcurrentExclusiveSchedulerPair`時就要指定最大併發數(類似於前面演示的`MaxConcurrencySynchronizationContext`);相反,在`ExclusiveScheduler`執行任務時,那麼將只允許執行一個排他任務,這個行為很像讀寫鎖。
和`SynchronizationContext`一樣,`TaskScheduler`也有一個`Current`屬性,會返回當前排程程式。不過,和`SynchronizationContext`不同的是,它沒有設定當前排程程式的方法,而是在啟動Task時就要提供,因為當前排程程式是與當前執行的Task相關聯的。所以,下方的示例程式會輸出“`True`”,這是因為和`StartNew`一起使用的lambda表示式是在`ConcurrentExclusiveSchedulerPair`的`ExclusiveScheduler`上執行的(我們手動指定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`提供了一個靜態的`FromCurrentSynchronizationContext`方法,該方法會建立一個`SynchronizationContextTaskScheduler`例項並返回,以便在原始的`SynchronizationContext.Current`上的`Post`方法對任務進行排隊執行。
#### SynchronizationContext和TaskScheduler是如何與await關聯起來的呢?
假設有一個UI App,它有一個按鈕。當點選按鈕後,會從網上下載一些文字並將其設定為按鈕的內容。我們應當只在UI執行緒中訪問該按鈕,因此當我們成功下載新的文字後,我們需要從擁有按鈕控制權的的執行緒中將其設定為按鈕的內容。如果不這樣做的話,會得到一個這樣的異常:
```
System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'
```
如果我們自己手動實現,那麼可以使用前面所述的`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執行緒上設定了按鈕的內容,與上面手動實現的版本一樣,`await Task`預設會關注`SynchronizationContext.Current`和`TaskScheduler.Current`兩個引數。當你在C#中使用`await`時,編譯器會進行程式碼轉換來向“可等待者”(這裡為`Task`)索要(通過呼叫`GetAwaiter`)“awaiter”(這裡為`TaskAwaiter`)。該awaiter負責掛接回調(通常稱為“繼續(continuation)”),當等待的物件完成時,該回調將被封送到狀態機,並使用在註冊回撥時捕獲的上下文或排程程式來執行此回撥。儘管與實際程式碼不完全相同(實際程式碼還進行了其他優化和調整),但大體上是這樣的:
```
object scheduler = SynchronizationContext.Current;
if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
{
scheduler = TaskScheduler.Current;
}
```
說人話就是,它先檢查有沒有設定當前`SynchronizationContext`,如果沒有,則再判斷當前排程程式是否為預設的`TaskScheduler`。如果不是,那麼當準備好呼叫回撥時,會使用該排程程式執行回撥;否則,通常會作為完成已等待任務的操作的一部分來執行回撥(譯註:這個“否則”我也沒看懂,我的理解是如果有當前上下文,則使用當前上下文執行回撥;如果當前上下文為空,且使用的是預設排程程式`ThreadPoolTaskScheduler`,則會啟用執行緒池執行緒執行回撥)。
#### ConfigureAwait(false)做了什麼?
`ConfigureAwait`方法並沒有什麼特別:編譯器或執行時均不會以任何特殊方式對其進行標識。它僅僅是一個返回結構體(`ConfiguredTaskAwaitable`)的方法,該結構體包裝了呼叫它的原始任務以及呼叫者指定的布林值。注意,`await`可以用於任何正確模式的型別(而不僅僅是Task,在C#中只要類包含`GetAwaiter()` 方法和`bool IsCompleted`屬性,並且`GetAwaiter()`的返回值包含 `GetResult()`方法、`bool IsCompleted`屬性和實現了 `INotifyCompletion`介面,那麼這個類的例項就是可以`await` 的)。當編譯器訪問例項的`GetAwaiter`方法(模式的一部分)時,它是根據`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;
}
}
```
換句話說,通過指定引數為`false`,即使有當前上下文或排程程式用於回撥,它也會假裝沒有。
#### 我為什麼要使用ConfigureAwait(false)?
`ConfigureAwait(continueOnCapturedContext: false)`用於避免強制在原始上下文或排程程式中進行回撥,有以下好處:
**提升效能**
比起直接呼叫,排隊進行回撥會更加耗費效能,一個是因為會有一些額外的工作(一般是額外的記憶體分配),另一個是因為無法使用我們本來希望在執行時中採用的某些優化(當我們確切知道回撥將如何呼叫時,我們可以進行更多優化,但如果將其移交給抽象的任意實現,則有時會受到限制)。對於大多數情況,即使檢查當前的`SynchronizationContext`和`TaskScheduler`也可能會增加一定的開銷(兩者都會訪問執行緒靜態變數)。如果`await`之後的程式碼並不需要在原始上下文中執行,那麼使用`ConfigureAwait(false)`就可以避免上述花銷:它不用排隊,且可以利用所有可以進行的優化,還可以避免不必要的執行緒靜態訪問。
**避免死鎖**
假如有一個方法,使用`await`等待網路下載結果,你需要通過同步阻塞的方式呼叫該方法等待其完成,比如使用`.Wait()`、`.Result`或`.GetAwaiter().GetResult()`。
思考一下,如果限制當前`SynchronizationContext`併發數為1,會發生什麼情況?方式不限,無論是顯式地通過類似於前面所說的`MaxConcurrencySynchronizationContext`的方式,還是隱式地通過僅具有一個可以使用的執行緒的上下文來實現,例如UI執行緒,你都可以在那個執行緒上呼叫該方法並阻塞它等待操作完成,該操作將開啟網路下載並等待。在預設情況下, 等待`Task`會捕獲當前`SynchronizationContext`,所以,當網路下載完成時,它會將回調排隊返回到`SynchronizationContext`中執行剩下的操作。但是,當前唯一可以處理排隊回撥的執行緒卻還被你阻塞著等待操作完成,不幸的是,在回撥處理完畢之前,該操作永遠不會完成。完蛋,死鎖了!
即使不將上下文併發數限制為1,而是通過其他任何方式對資源進行了限制,結果也是如此。比如,我們將`MaxConcurrencySynchronizationContext`限制為4,這時,我們對該上下文進行4次排隊呼叫,每個呼叫都會進行阻塞等待操作完成。現在,我們在等待非同步方法完成時仍阻塞了所有資源,這些非同步方法能否完成取決於是否可以在已經完全消耗掉的上下文中處理它們的回撥。哦吼,又死鎖了!
如果該方法改為使用`ConfigureAwait(false)`,那麼它就不會將回調排隊送回原始上下文,進而避免了死鎖。
#### 我為什麼要使用ConfigureAwait(true)?
**絕對沒必要使用**,除非你閒的蛋疼使用它來表明你是故意不使用`ConfigureAwait(false)`的(例如消除VS的靜態分析警告或類似的警告等),使用`ConfigureAwait(true)`沒有任何意義。`await task`和`await task.ConfigureAwait(true)`在功能上沒有任何區別,如果你在生產環境的程式碼中發現了`ConfigureAwait(true)`,那麼你可以直接刪除它,不會有任何副作用。
`ConfigureAwait`方法接收一個布林值引數,可能在某些特殊情況下,你需要通過傳入變數來控制配置,不過,99%的情況下都是通過硬編碼的方式傳入的,如`ConfigureAwait(false)`
#### 什麼時候應該使用ConfigureAwait(false)?
這取決於:你在實現應用程式級程式碼還是通用庫程式碼?
當你編寫應用程式時,你通常需要使用預設行為(這就是`ConfigureAwait(true)`是預設行為的原因(譯註:原作者應該是想要表達編寫應用程式比通用庫更加頻繁,所以該行為會更頻繁的使用))。如果應用模型或環境(例如WinForm,WPF,ASP.NET Core等)釋出了自定義`SynchronizationContext`,那麼基本上可以肯定有一個很好的理由:它為關注同步上下文的程式碼提供了一種與應用模型或環境適當互動的方式。所以如果你使用WinForm寫事件處理器、在xunit中寫單元測試或在ASP .NET MVC控制器中編碼,無論應用程式模型是否確實釋出了`SynchronizationContext`,您都想使用該`SynchronizationContext`(如果存在),那麼您可以簡單地`await`預設的`ConfigureAwait(true)`,如果存在回撥,就可以將其正確地封送到原始上下文中執行。這就形成了以下一般指導:**如果您正在編寫應用程式級程式碼,請不要使用`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`需要在原始上下文中執行,但如果程式碼違反了該準則,在錯誤的情況下使用了`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;
}
```
這將導致出現錯誤的結果。依賴於`HttpContext.Current`的經典ASP.NET應用程式中的程式碼也是如此,使用`ConfigureAwait(false)`然後嘗試使用`HttpContext.Current`也可能會導致問題。
相反,通用庫之所以成為“通用庫”,原因之一是因為它們不關心使用它們的環境。您可以在Web應用程式、客戶端應用程式或測試程式中使用它們,這無關緊要,因為庫程式碼與可能使用的應用程式模型無關。那麼,無關就意味著它不會做任何需要以特定方式與應用程式模型進行互動的事情,例如:它不會訪問UI控制元件,因為通用庫對UI控制元件一無所知。由於我們不需要在任何特定環境中執行程式碼,那麼我們可以避免將回調強制送回到原始上下文,這可以通過使用`ConfigureAwait(false)`來實現,並享受到其帶來的效能和可靠性優勢。這形成了以下一般指導:**如果要編寫通用庫程式碼,請使用`ConfigureAwait(false)`**。這就是為什麼您會在`.NET Core`執行時庫中看到每個(或幾乎每個)`await`時都要使用`ConfigureAwait(false)`的原因;如果不是這樣的話(除了少數例外),那很可能是一個要修復的BUG。例如,[此Pull request](https://github.com/dotnet/corefx/pull/38610)修復了`HttpClient`中缺少的`ConfigureAwait(false)`呼叫。
當然,與其他指導一樣,在某些特殊的情況下可能不適用。例如,在通用庫中,具有可呼叫委託的API是一個較大的例外(或至少需要考慮的例外)。在這種情況下,庫的呼叫者可能會傳遞由庫呼叫的應用程式級程式碼,然後有效地呈現了庫那些“通用”假設。例如,以LINQ中`Where`的非同步版本(執行時庫不存在該方法,僅僅是假設)為例:`public static async IAsyncEnumerable WhereAsync(this IAsyncEnumerable source, Func predicate)`。這裡的`predicate`是否需要在呼叫者的原始`SynchronizationContext`上重新呼叫?這要取決於`WhereAsync`的實現,因此,它可能選擇不使用`ConfigureAwait(false)`。
即使有這些特殊情況,一般指導仍然是一個很好的起點:如果要編寫通用庫或與應用程式模型無關的程式碼,請使用`ConfigureAwait(false)`,否則請不要這樣做。
#### 以下是一些常見問題
##### ConfigureAwait(false)能保證回撥不會在原始上下文中執行嗎?
**並不能保證**!它雖能保證它不會被排隊回到原始上下文中……但這並不意味著`await task.ConfigureAwait(false)`後的程式碼仍不會在原始上下文中執行。因為當等待已經完成的可等待物件時(即Task例項返回時該Task已經完成了),後續程式碼將會保持同步執行,而無需強制排隊等待。所以,如果您等待的任務在等待時就已經完成了,那麼無論您是否使用了`ConfigureAwait(false)`,緊隨其後的程式碼也會在擁有當前上下文的當前執行緒上繼續執行。
##### 我的方法中僅在第一次`await`時使用`ConfigureAwait(false)`而剩下的程式碼不使用可以嗎?
一般來說,不行,參考前面的FAQ。如果`await task.ConfigureAwait(false)`在等待時就已完成了(實際上很常見),那麼`ConfigureAwait(false)`將毫無意義,因為執行緒在此之後繼續在該方法中執行程式碼,並且仍在與之前相同的上下文中執行。
有一個例外是:如果您知道第一次等待始終會非同步完成,並且正在等待的事物會在沒有自定義`SynchronizationContext`或`TaskScheduler`的環境中呼叫其回撥。例如,.NET執行時庫中的`CryptoStream`希望確保其潛在的計算密集型程式碼不會被呼叫者以同步方式進行呼叫,因此它使用[自定義的`awaiter`](https://github.com/dotnet/runtime/blob/4f9ae42d861fcb4be2fcd5d3d55d5f227d30e723/src/libraries/System.Security.Cryptography.Primitives/src/System/Security/Cryptography/CryptoStream.cs#L205)來確保第一次等待後的所有內容都線上程池執行緒上執行。但是,即使在這種情況下,您也會注意到下一次等待仍將使用`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`內部你會發現`SynchronizationContext.Current`就是`SomeCoolSyncCtx`例項,並且該`await`和`SomethingAsync`內部的所有未配置的`await`都將返回到該上下文。因此,要使用這種方式,您需要了解排隊的所有程式碼可能會做什麼或不做什麼,以及它的行為是否會阻礙您的行為。
這種方法還需要以建立或排隊其他任務物件為代價。這取決於您的效能敏感性,對您的應用程式或庫而言可能無關緊要。
另外要注意,這些技巧可能會引起更多的問題,並帶來其他意想不到的後果。例如,靜態分析工具(例如Roslyn分析儀)提供了標記不使用`ConfigureAwait(false)`的標誌等待,正如[CA2007](https://docs.microsoft.com/en-us/visualstudio/code-quality/ca2007?view=vs-2019)。如果啟用了這樣的分析器,並採用該技巧來避免使用`ConfigureAwait`,那麼分析器很有可能會標記它,這其實會給您帶來更多工作。那麼,也許您可能會因為其煩擾而禁用了分析器,這將會導致您忽略程式碼庫中實際上應該一直使用`ConfigureAwait(false)`的其他程式碼。
##### 我能用SynchronizationContext.SetSynchronizationContext來避免使用ConfigureAwait(false)嗎?
**不行!** 額。。好吧,也許可以。這取決於你寫的程式碼。可能一些開發者這樣寫:
```
Task t;
var old = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
try
{
t = CallCodeThatUsesAwaitAsync(); // 在方法內部進行 await 不會感知到原始上下文
}
finally
{
SynchronizationContext.SetSynchronizationContext(old);
}
await t; // 這時則會回到原始上下文
```
我們希望`CallCodeThatUsesAwaitAsync`中的程式碼看到的當前上下文是null,而且確實如此。但是,以上內容不會影響`TaskScheduler`的等待狀態,因此,如果此程式碼在某些自定義`TaskScheduler`上執行,那麼在`CallCodeThatUsesAwaitAsync`(不使用`ConfigureAwait(false)`)內部等待後仍將排隊返回該自定義`TaskScheduler`。
所有這些注意事項也適用於前面`Task.Run`相關的FAQ:這種解決方法可能會帶來一些效能方面的問題,並且try中的程式碼也可以通過設定其他上下文(或使用非預設TaskScheduler來呼叫程式碼)來阻止這種嘗試。
使用這種模式,您還需要注意一些細微的變化:
```
var old = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
try
{
await t;
}
finally
{
SynchronizationContext.SetSynchronizationContext(old);
}
```
找到問題沒?可能很難發現但是影響很大。這樣寫沒法保證`await`最終會回到原始執行緒上執行回撥並繼續執行生下的程式碼,也就是說將`SynchronizationContext`重置回原始上下文這個操作可能實際上並未在原始執行緒上進行,這可能導致該執行緒上的後續工作項看到錯誤的上下文(為解決這一問題,具有良好編碼規範的應用模型在設定了自定義上下文時,通常會在呼叫任何其他使用者程式碼之前新增程式碼以手動將其重置)。而且即使它確實在同一執行緒上執行,也可能要等一會兒,這樣一來,上下文仍無法適當恢復。而且,如果它在其他執行緒上執行,可能最終會在該執行緒上設定錯誤的上下文。等等。很不理想。
##### 如果我用了`GetAwaiter().GetResult()`,我還需要使用`ConfigureAwait(false)`嗎?
**不需要**,`ConfigureAwait`隻影響回撥。具體來說,awaiter模式要求awaiters 公開`IsCompleted`屬性、`GetResult`方法和`OnCompleted`方法(可選使用`UnsafeOnCompleted`方法)。`ConfigureAwait`只會影響`OnCompleted/UnsafeOnCompleted`的行為,因此,如果您只是直接呼叫等待者的`GetResult()`方法,那麼你無論是在`TaskAwaiter`上還是在`ConfiguredTaskAwaitable.ConfiguredTaskAwaiter`上進行操作,都是沒有任何區別的。因此,如果在程式碼中看到`task.ConfigureAwait(false).GetAwaiter().GetResult()`,則可以將其替換為`task.GetAwaiter().GetResult()`(並考慮是否真的需要這樣的阻塞)。
##### 我知道我的執行環境永遠不會具有自定義SynchronizationContext或自定義TaskScheduler
我可以跳過使用ConfigureAwait(false)嗎?
**也許可以,這取決於你是如何確定“永遠不會”的。** 如之前的FAQ,僅僅因為您正在使用的應用程式模型未設定自定義`SynchronizationContext`且未在自定義`TaskScheduler`上呼叫您的程式碼並不意味著其他使用者或庫程式碼未設定。因此,您需要確保不存在這種情況,或至少要意識到這種風險。
##### 我聽說在 .NET Core中ConfigureAwait(false)已經不再需要了,這是真的嗎?
**假的!** 在.NET Core上執行時仍需要使用它,和在.NET Framework上執行時需要使用的原因完全相同,在這方面沒有任何改變。
不過,有一些變化的是某些環境是否釋出了自己的`SynchronizationContext`。特別是雖然在.NET Framework上的經典ASP.NET具有自己的`SynchronizationContext`,但是ASP.NET Core卻沒有。這意味著預設情況下,在ASP.NET Core應用程式中執行的程式碼是看不到自定義`SynchronizationContext`的,從而減少了在這種環境中執行`ConfigureAwait(false)`的需要。
但這並不意味著永遠不會存在自定義的`SynchronizationContext`或`TaskScheduler`。如果某些使用者程式碼(或您的應用程式正在使用的其他庫程式碼)設定了自定義上下文並呼叫了您的程式碼,或在自定義`TaskScheduler`的預定`Task`中呼叫您的程式碼,那麼即使在ASP.NET Core中,您的等待物件也可能會看到非預設上下文或排程程式,從而促使您想要使用`ConfigureAwait(false)`。當然,在這種情況下,如果您想要避免同步阻塞(任何情況下,都應避免在Web應用程式中進行同步阻塞),並且不介意在這種有限的情況下有細微的效能開銷,那您可能無需使用`ConfigureAwait(false)`就可以實現。
##### 我在await using一個IAsyncDisposable的物件時我可以使用ConfigureAwait嗎?
**可以,不過有些小問題。** 與前面的FAQ中所述的`IAsyncEnumerable`一樣,.NET執行時公開了一個`IAsyncDisposable`的擴充套件方法`ConfigureAwait `的擴充套件方法,並且`await using`能很好地與此一起工作,因為它實現了適當的模式(即公開了適當的DisposeAsync方法):
```
await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false))
{
...
}
```
這裡的問題是,變數c的型別現在不是`MyAsyncDisposableClass`,而是`System.Runtime.CompilerServices.ConfiguredAsyncDisposable`,這是從`IAsyncDisposable`上的`ConfigureAwait`擴充套件方法返回的型別。
為了解決這個問題,您需要多寫一行:
```
var c = new MyAsyncDisposableClass();
await using (c.ConfigureAwait(false))
{
...
}
```
現在,變數c的型別又是所需的`MyAsyncDisposableClass`了。這還具有增加c範圍的作用;如果有影響,則可以將整個內容括在大括號中。
##### 我使用了ConfigureAwait(false),但是我的AsyncLocal在等待之後仍然流向程式碼,那是個BUG嗎?
**不,這是預期的。** `AsyncLocal`資料流是`ExecutionContext`的一部分,它與`SynchronizationContext`是相互獨立的。除非您使用`ExecutionContext.SuppressFlow()`明確禁用了`ExecutionContext`流,否則`ExecutionContext`(以及`AsyncLocal`資料)將始終在等待狀態中流動,無論是否使用`ConfigureAwait`來避免捕獲原始的`SynchronizationContext`。有關更多資訊,請參見此[部落格](https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/)。
##### 可以在語言層面幫助我避免在我的庫中顯式使用ConfigureAwait(false)嗎?
類庫開發人員有時會對需要使用`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
如果這對您很重要,或者您有新的有趣的想法,我鼓勵您為這些或新的討論貢獻自己的想法。