2015-12-15 51 views
3

我在C#中使用了一些線程構造,並遇到了一些無法理解鎖定工作方式的東西。我有一個輔助函數,它接受一個異步任務,並使用一個TaskCompletionSource成員在多次調用時嘗試同步訪問。第二個線程在第一個線程釋放之前進入鎖定狀態

public static void Main(string[] args) 
{ 
    var test = new TestClass(); 
    var task1 = test.Execute("First Task", async() => await Task.Delay(1000)); 
    var task2 = test.Execute("Second Task", async() => await Task.Delay(1000)); 

    task1.Wait(); 
    task2.Wait(); 

    Console.ReadLine(); 
} 

class TestClass : IDisposable 
{ 
    private readonly object _lockObject = new object(); 
    private TaskCompletionSource<bool> _activeTaskCompletionSource; 

    public async Task Execute(string source, Func<Task> actionToExecute) 
    { 
     Task activeTask = null; 
     lock (_lockObject) 
     { 
      if (_activeTaskCompletionSource != null) 
      { 
       activeTask = _activeTaskCompletionSource.Task; 
      } 
      else 
      { 
       _activeTaskCompletionSource = new TaskCompletionSource<bool>(); 
      } 
     } 

     while (activeTask != null) 
     { 
      await activeTask; 
      lock (_lockObject) 
      { 
       if (_activeTaskCompletionSource != null) 
       { 
        activeTask = _activeTaskCompletionSource.Task; 
       } 
       else 
       { 
        activeTask = null; 
       } 
      } 
     } 

     await actionToExecute(); 
     lock (_lockObject) 
     { 
      _activeTaskCompletionSource.SetResult(true); 
      _activeTaskCompletionSource = null; 
     } 
    } 
} 

這總是最終落入第二個任務的無限循環。我把一些代碼來記錄每一個步驟,因爲它發生,它總是會產生這樣的事情(我#秒後已經手動插入註釋):

 
[First Task] Waiting for lock (setup) 
[First Task] Entered lock (setup) 
[First Task] Grabbing '_activeTaskCompletionSource' (setup) 
[First Task] Lock released (setup) 
[First Task] RUNNING ... 
[Second Task] Waiting for lock (setup) 
[Second Task] Entered lock (setup) 
[Second Task] Assigning 'activeTask' (setup) 
[Second Task] Lock released (setup) 
[Second Task] Waiting for task to complete ... 
[First Task] COMPLETED! 
[First Task] Waiting for lock (cleanup) 
[First Task] Entered lock (cleanup) 
[First Task] Setting _activeTaskCompletionSource result ... 
    # Never gets to '_activeTaskCompletionSource = null' 
    # Never gets to 'Releasing lock (cleanup)' for first task 
[Second Task] Awaited task completed! 
[Second Task] Waiting for lock (loop) 
    # Immediately enters lock after 'await' is complete 
    # Does not wait for 'First Task' to finish its lock! 
[Second Task] Entered lock (loop) 
[Second Task] Assigning 'activeTask' (loop) 
[Second Task] Lock released (loop) 
[Second Task] Waiting for task to complete ... 
[Second Task] Awaited task completed! 

這最終將第二個任務進入一個無限循環,因爲_activeTaskCompletionSource永遠不會回到null

我是沒有其他線程所能進入的鎖,直到所有先前的線程都離開它的印象,但在這裏,我First Task線程永遠不會完成和Second Task線程爭奪持有它之前釋放其清理鎖。

這是否與混合鎖和異步/等待有關?

+0

它絕對不是建議先task.Wait()或task.Result到一個異步任務,因爲它可以很容易地在一個僵局的情況下結束。 –

+0

@OsmanEsen本例中的'task.Wait()'調用僅用於它在控制檯應用程序中運行,其中_impossible_在主函數中使用async/await。我避免了在所有的異步函數本身以及整個TestClass實現中使用阻塞調用。 – KChaloux

回答

3

調用TaskCompletionSource.SetResult可能會內聯繼續,導致意外和任意代碼在鎖下運行。 await也使用延續。

這個討厭的行爲是TPL中的一個設計錯誤。如果你關心這個there is a GitHub issue。在那裏留言。

+0

特別是如果您在.NET 4.6上,您可以通過將完成源的創建更改爲_activeTaskCompletionSource = new TaskCompletionSource (TaskCreationOptions.RunContinuationsAsynchronously);' –

+0

Ouch!這很痛苦。我認爲這是我做錯了,而不是框架本身的東西。謝謝(你的)信息。 – KChaloux

2

由於boot4Life points out,這是一個設計錯誤,該框架允許從SetResult繼續運行在同一個線程上。

要解決它,如果你是在.NET 4.6更改完成源的創建到

_activeTaskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); 

這阻止這樣的事情發生。如果你只是在4.5.x上,你必須在新線程上運行完成,以防止它被拾取。

lock (_lockObject) 
    { 
     var completionSource = _activeTaskCompletionSource; 
     _activeTaskCompletionSource = null; 
     Task.Run(() => completionSource.SetResult(true));    
    } 
+0

不幸的是,我被困在4.5,所以它看起來像你的解決方案,它在自己的線程上運行它將被要求。 – KChaloux

相關問題