簡體   English   中英

ConfigureAwait 將延續推送到池線程

[英]ConfigureAwait pushes the continuation to a pool thread

這是一些 WinForms 代碼:

async void Form1_Load(object sender, EventArgs e)
{
    // on the UI thread
    Debug.WriteLine(new { where = "before", 
        Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });

    var tcs = new TaskCompletionSource<bool>();

    this.BeginInvoke(new MethodInvoker(() => tcs.SetResult(true)));

    await tcs.Task.ContinueWith(t => { 
        // still on the UI thread
        Debug.WriteLine(new { where = "ContinueWith", 
            Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });
    }, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false);

    // on a pool thread
    Debug.WriteLine(new { where = "after", 
        Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });
}

輸出:

{ where = before, ManagedThreadId = 10, IsThreadPoolThread = False }
{ where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False }
{ where = after, ManagedThreadId = 11, IsThreadPoolThread = True }

為什么 ConfigureAwait 會主動將await繼續推送到這里的池線程?

我用“推到池中的線程”在這里當主延續回調(的描述的情況下action參數TaskAwaiter.UnsafeOnCompleted被調用在一個線程,而二次回調(一傳給ConfiguredTaskAwaiter.UnsafeOnCompleted )排隊到一個池線程。

文檔說:

continueOnCapturedContext ... true 嘗試將延續編組回捕獲的原始上下文; 否則為假。

我知道當前線程上安裝了WinFormsSynchronizationContext 盡管如此,沒有嘗試進行編組,執行點已經存在。

因此,它更像是“永遠不要繼續捕獲的原始上下文” ......

正如預期的那樣,如果執行點已經在沒有同步上下文的池線程上,則沒有線程切換:

await Task.Delay(100).ContinueWith(t => 
{ 
    // on a pool thread
    Debug.WriteLine(new { where = "ContinueWith", 
        Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });
}, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false);
{ where = before, ManagedThreadId = 10, IsThreadPoolThread = False }
{ where = ContinueWith, ManagedThreadId = 6, IsThreadPoolThread = True }
{ where = after, ManagedThreadId = 6, IsThreadPoolThread = True }

已更新,再進行一項測試以查看是否有任何同步。 上下文不足以繼續(而不是原始的)。 確實是這樣:

class DumbSyncContext: SynchronizationContext
{
}

// ...

Debug.WriteLine(new { where = "before", 
    Thread.CurrentThread.ManagedThreadId, 
    Thread.CurrentThread.IsThreadPoolThread });

var tcs = new TaskCompletionSource<bool>();

var thread = new Thread(() =>
{
    Debug.WriteLine(new { where = "new Thread",                 
        Thread.CurrentThread.ManagedThreadId,
        Thread.CurrentThread.IsThreadPoolThread});
    SynchronizationContext.SetSynchronizationContext(new DumbSyncContext());
    tcs.SetResult(true);
    Thread.Sleep(1000);
});
thread.Start();

await tcs.Task.ContinueWith(t => {
    Debug.WriteLine(new { where = "ContinueWith",
        Thread.CurrentThread.ManagedThreadId,
        Thread.CurrentThread.IsThreadPoolThread});
}, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false);

Debug.WriteLine(new { where = "after", 
    Thread.CurrentThread.ManagedThreadId, 
    Thread.CurrentThread.IsThreadPoolThread });
{ where = before, ManagedThreadId = 9, IsThreadPoolThread = False }
{ where = new Thread, ManagedThreadId = 10, IsThreadPoolThread = False }
{ where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False }
{ where = after, ManagedThreadId = 6, IsThreadPoolThread = True }

為什么 ConfigureAwait 在這里主動將 await 繼續推送到池線程?

它不會“將它推送到線程池線程”,而是說“不要強迫自己回到之前的SynchronizationContext ”。

如果您不捕獲現有上下文,則處理該await之后的代碼的延續將只在線程池線程上運行,因為沒有要編組回的上下文。

現在,這與“推送到線程池”略有不同,因為當您執行ConfigureAwait(false)時,不能保證它會在線程池上運行。 如果你打電話:

await FooAsync().ConfigureAwait(false);

FooAsync()可能會同步執行,在這種情況下,您將永遠不會離開當前上下文。 在這種情況下, ConfigureAwait(false)沒有實際效果,因為由await功能創建的狀態機將短路並直接運行。

如果您想看到這一點,請創建一個像這樣的異步方法:

static Task FooAsync(bool runSync)
{
   if (!runSync)
       await Task.Delay(100);
}

如果你這樣稱呼它:

await FooAsync(true).ConfigureAwait(false);

您將看到您停留在主線程上(假設它是等待之前的當前上下文),因為在代碼路徑中沒有實際執行的異步代碼。 FooAsync(false).ConfigureAwait(false);相同的調用FooAsync(false).ConfigureAwait(false); 但是,會導致它在執行后跳轉到線程池線程。

這是基於挖掘.NET Reference Source對這種行為的解釋。

如果使用ConfigureAwait(true) ,則繼續通過使用SynchronizationContextTaskScheduler TaskSchedulerAwaitTaskContinuation完成,在這種情況下一切都很清楚。

如果使用ConfigureAwait(false) (或者如果沒有同步上下文要捕獲),則通過AwaitTaskContinuation完成,它首先嘗試內聯延續任務,然后在無法內聯時使用ThreadPool將其排隊。

內聯由IsValidLocationForInlining確定,它從不使用自定義同步上下文將任務內聯到線程上。 然而,它最好將它內聯到當前池線程上。 這解釋了為什么我們在第一種情況下被推送到池線程上,而在第二種情況下(使用Task.Delay(100) )保持在同一個池線程上。

我認為最容易以稍微不同的方式思考這一點。

假設你有:

await task.ConfigureAwait(false);

首先,如果task已經完成,那么正如 Reed 指出的那樣,實際上會忽略ConfigureAwait並繼續執行(同步,在同一線程上)。

否則, await將暫停該方法。 在這種情況下,當await恢復並看到ConfigureAwaitfalse ,有特殊的邏輯來檢查代碼是否具有SynchronizationContext ,如果是,則在線程池上恢復。 這是無證但並非不當行為。 因為它是無證的,我建議你不要依賴於行為; 如果你想在線程池上運行一些東西,使用Task.Run ConfigureAwait(false)字面意思是“我不在乎這個方法在什么上下文中恢復。”

請注意, ConfigureAwait(true) (默認值)將在當前SynchronizationContextTaskScheduler上繼續該方法。 雖然ConfigureAwait(false)將在任何線程上繼續該方法,除了具有SynchronizationContext線程。 它們並不完全相反。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM