2014-10-07 50 views
4

當在異步函數中使用不同的SynchronizationContext而不是外部時,我遇到了令人困惑的行爲。等待不使用當前的SynchronizationContext

我的程序的大部分代碼都使用一個自定義的SynchronizationContext,它只是將SendOrPostCallbacks排隊,並在我的主線程的特定已知點處調用它們。我在開始的時候設置了這個自定義的SynchronizationContext,當我只使用這個時,一切正常。

我遇到的問題是我有我希望他們等待繼續在線程池中運行的函數。

void BeginningOfTime() { 
    // MyCustomContext queues each endOrPostCallback and runs them all at a known point in the main thread. 
    SynchronizationContext.SetSynchronizationContext(new MyCustomContext()); 


    // ... later on in the code, wait on something, and it should continue inside 
    // the main thread where MyCustomContext runs everything that it has queued 
    int x = await SomeOtherFunction(); 
    WeShouldBeInTheMainThreadNow(); // ********* this should run in the main thread 
} 

async int SomeOtherFunction() { 
    // Set a null SynchronizationContext because this function wants its continuations 
    // to run in the thread pool. 
    SynchronizationContext prevContext = SynchronizationContext.Current; 
    SynchronizationContext.SetSynchronizationContext(null); 

    try { 

     // I want the continuation for this to be posted to a thread pool 
     // thread, not MyCustomContext. 
     await Blah(); 

     WeShouldBeInAThreadPoolThread(); // ********* this should run in a thread pool thread 

    } finally { 
     // Restore the previous SetSynchronizationContext. 
     SynchronizationContext.SetSynchronizationContext(prevContext); 
    } 
} 

我得到的行爲是每個await後面的代碼都是在一個看似隨機的線程中執行的。有時候,WeShouldBeInTheMainThreadNow()運行在線程池線程中,有時運行在主線程中。有時WeShouldBeInAThreadPoolThread()正在運行

我沒有在這裏看到一個模式,但我認爲無論什麼SynchronizationContext.Current被設置在你使用await的那一行是將定義在await後面的代碼的位置執行。這是一個不正確的假設嗎?如果是這樣,有沒有一種緊湊的方式來做我想在這裏做的事情?

+0

有沒有保證,如果任務(或其他awaitable)實際上已經被你執行'await'的時間內完成將發生的任何線程切換 - 永遠記住,代碼可以止矣跑過這一點。 – 2014-10-07 14:29:50

回答

1

關於await有一個常見的誤解,那就是專門處理一個async函數。

但是,await關鍵字在一個對象上運行,它根本不關心等待對象來自何處。

也就是說,你可以隨時與var blahTask = Blah(); await blahTask;

當你重寫外await呼叫方式會發生什麼改寫await Blah();

// Synchronization Context leads to main thread; 
Task<int> xTask = SomeOtherFunction(); 
// Synchronization Context has already been set 
// to null by SomeOtherFunction! 
int x = await xTask; 

,然後還有其他的問題:從內法finally在繼續被執行,這意味着它是在線程池執行 - 這樣不僅你有沒有設置你的SynchronizationContext,但你的SynchronizationContext將會(有可能)在未來某個時候在另一個線程上恢復。但是,因爲我不是很瞭解SynchronizationContext的流動方式,所以很可能SynchronizationContext根本不會恢復,它只是設置在另一個線程上(請記住,SynchronizationContext.Current是線程本地...)

這兩個問題相結合,很容易解釋你觀察到的隨機性。 (也就是說,您正在從多個線程操縱準全局狀態...)

問題的根源在於await關鍵字不允許調度延續任務。

一般情況下,你只是想說明「這不是爲await後的代碼很重要的是在相同的上下文await之前的代碼」,在這種情況下,使用ConfigureAwait(false)將是適當的;

async Task SomeOtherFunction() { 
    await Blah().ConfigureAwait(false); 
} 

但是,如果你絕對要指定「我希望await後的代碼在線程池中運行」 - 這是一件好事,應該是罕見的,那麼你就不能await做到這一點,但你可以做到這一點,例如與ContinueWith - 但是,你會混合使用多種方式使用Task對象,這可能會導致相當混亂的代碼。

Task SomeOtherFunction() { 
    return Blah() 
     .ContinueWith(blahTask => WeShouldBeInAThreadPoolThread(), 
         TaskScheduler.Default); 
} 
+0

關於'finally'在線程池上下文中執行的好處,但我必須強烈反對'ContinueWith'的建議。 'ConfigureAwait(false)'就足夠了。 – 2014-10-07 13:08:43

+0

@StephenCleary:我不知道'Blah()'在哪裏執行。如果'Blah()'在線程池上執行,那麼你是對的。但是如果不是這樣(例如,如果它是一個I/O任務,那麼我猜這個任務將在一個I/O完成端口上完成 - 但是不能以任何方式保證),那麼這個延續將不會被安排在線程池。然而,對我來說真正的困惑是爲什麼需要控制* continuation *任務的執行位置。 – 2014-10-07 13:13:56

+2

常見的概念模型是代碼運行在特定的上下文中(在這種情況下,自定義'SynchronizationContext')或「別的地方」。無論是「其他地方」是常規線程池線程還是IOCP線程池線程幾乎都不重要。 – 2014-10-07 13:18:30

2

我希望你的代碼工作,但有幾個可能的原因,它不是:

  1. 確保您SynchronizationContext是當前當它執行它的延續。
  2. 沒有嚴格定義SynchronizationContext被捕獲。
  3. SynchronizationContext中運行代碼的正常方法是在一個方法中建立當前的一個,然後運行依賴於它的另一個(可能是異步的)方法。
  4. 避免當前SynchronizationContext的正常方法是將ConfigureAwait(false)附加到所有正在等待的任務。