.NET 非同步工作的延續

這篇文章簡單介紹了 .NET 非同步工作的延續(continuation),以及取消延續工作的一個特殊案例。

圖片來源:pngitem.com

這裡所謂「工作的延續」,指的是等到某個非同步工作執行完畢之後才要接著執行的工作;你要它稱為非同步工作的「延續工作」或「接續工作」也可以。有了 asyncawait 語法的幫忙,在處理延續工作這件事情上,程式寫起來變得輕鬆許多。

假設某些情況下,我們無法使用 asyncawait 語法,此時雖然能夠使用 TaskResult 屬性來等待工作的執行結果,或者呼叫 Wait 方法來等待工作完成,但它們都會阻擋當前的執行緒,如此便失去了非同步工作的意義。那麼,是否還有其他方法能夠通知我們某個非同步工作已經完成、以便繼續執行後續的工作?答案就是 Task 相關類別所提供的 ContinueWith 多載方法。

ContinueWith 方法可以讓我們輕易的銜接兩個非同步工作,例如:


ContinueWith 方法會建立新的 Task 物件來封裝接續的工作,而且這些工作在創建之時,是處於等待狀態(TaskStatus.WaitingForActivation),須等到前項工作(如上面範例的 webDownloadTask)執行完畢才會開始執行。此外,在預設情況下,透過 ContinueWith 方法所建立的工作都會動用到執行緒(由 TaskScheduler 從執行緒集區調用執行緒);也就是說,它們都會在各自的執行緒上面執行(無論前項工作是否有動用執行緒)。

由於 ContinueWith 方法會傳回它所建立的 Task 物件,而該物件也能夠拿來做為其他工作的前項工作(antecedent),故可層層串接,不斷延續。例如:task.ContinueWith(...).ContinueWith(...)

串接與組合多項工作

實務上,應用程式執行時可能會起始多個非同步工作,而這些工作之間亦可能有先後順序的關係,例如工作 A 執行完畢之後才能執行 B 和 C——亦即非同步工作 B 和 C 是工作 A 的延續。透過 ContinueWith 方法,我們便能夠串接任意數量的非同步工作,並確保這些非同步工作的執行順序,或者把多個小工作組合起來完成一項複雜工作。參考以下範例:


在此範例中:
  • taskA 起始之後,第 8~13 行有一個延續工作,而等到 taskA 完成之後,會接著起始兩個延續工作:taskB 和 taskC。對 B 和 C 而言,A 是他們的前項工作。
  • 前項工作執行完畢後,以 ContinueWith 方法所指定的延續工作將自動以非同步的方式開始執行。此例的 taskA 執行完畢之後,接著會分別以非同步的方式起始 taskB 和 taskC;至於哪一個延續工作會先執行則不一定(無法在編譯時期確定)。
  • ContinueWith 方法所指定的延續工作可以透過參數 antecedentTask 來獲取前項工作,以便得知前項工作當前的狀態。

此範例程式的執行結果如下(在你的機器上執行時,接續工作 B 和 C 的順序可能不同):


TaskContinuationOptions

Task.ContinueWith 方法有數個多載版本,其中有些可以傳入列舉型別 TaskContinuationOptions 所組成的旗號來改變預設的延續行為。比如說,你可以使用 OnlyOnRanToCompletion 旗號來確保唯有在前項工作順利完成(沒有發生錯誤)的情況下才能執行延續工作,或者用 OnlyOnFaultedOnlyOnCanceled來指定唯有在前項工作「發生錯誤」或者「被取消」的情況。又如 NotOnRanToCompletion,則是限定前項工作「沒有順利完成」的情況才能執行延續工作。
TaskContinuationOptions 的完整定義可參考 MSDN 線上文件
底下是一個簡單範例,示範如何利用 TaskContinuationOptions 來設定只有當前項工作發生某種狀況時才去觸發(起始)某個延續工作——這等於是非同步的事件模型。範例程式如下:


在此範例中,唯有當 taskA 順利執行完畢,才會接著起始 taskOnCompleted 所代表的非同步工作。如果 taskA 執行失敗,則只有 taskOnFailed 工作會接著執行。

當你碰到需要「射後不理」(fire-and-forget)的非同步工作時,這種類似觸發事件的寫法便可派上用場。
你還可以用 ExecuteSynchronously 旗號來告訴工作排程器:當前項工作執行完畢時,請立刻以當前的執行緒來執行此延續工作,而不要將它排入佇列,亦即不要將它執行於另一條執行緒。一般而言,只有在延續工作需要盡快執行的情況才需要使用這個旗號。

取消「中間的」延續工作

預設情況下,當你取消一項工作,則與該項工作關聯的「延續工作」都會立刻變成可執行的狀態(這句話可以簡單理解成:一旦前項工作取消,它的子工作便會「幾乎」立刻接著執行)。

考慮一個場景:假設 TaskB 是 TaskA 的延續工作,而 Task C 又是 TaskB 的延續工作,即三者的執行順序是 TaskA 然後 TaskB 然後 TaskC。理論上,延續工作應該會等到前項工作執行完畢之後才會執行,故 TaskB 以及 TaskC 應該都是在 TaskA 完成之後才會執行。

然而,當 TaskB 被中途取消、而且 TaskA 尚未完成,此時便很可能會出現一個特殊情形:TaskA 和 TaskC 都會繼續執行,可是 TaskC 會比它的「前項的前項工作」TaskA 更早執行完畢。請看以下範例:


執行結果:
TaskC 執行完畢。
TaskA 執行完畢。 

在此範例中,taskA 起始之後,接著立刻建立了延續工作 taskB,然後又建立 taskB 的延續工作 taskC,最後緊接著取消 taskB(第 11 行)。這裡用 ThreadSleep 方法來確保 taskA 要花一秒以上的時間才能執行完畢,故在取消 taskB 的時候,taskA 肯定還沒結束;在此同時,taskC 會因為前項工作 taskB 的取消而立刻開始執行,於是便會形成 taskC 比最初之前項工作 taskA 更早執行完畢的情形。

如果你要希望 taskA 不會因為子工作 taskB 的取消而導致「孫代」工作 taskC 提前執行,可以在建立 taskB 的時候使用 TaskContinuationOptionsLazyCancellation 旗號來告訴工作排程器:當我(taskB)被取消時,請延後這個取消動作,直到前項工作結束之後才執行取消動作。故前面範例可以修改成:


結果就會變成 TaskA 先執行完畢,然後才是 TaskC:
TaskA 執行完畢。
TaskC 執行完畢。 

參考資料


  • Programming C# 8.0 by Ian Griffiths
技術提供:Blogger.