在C#中,可以使用CancellationToken
和Task
的超時機制來實現調用方法時的超時終止。
一
用Task.Delay(int)模擬耗時操作
static async Task Main(string[] args){using (var cts = new CancellationTokenSource(1 * 1000)){await doSomething(cts.Token);}Console.WriteLine("Press any key to end...");Console.ReadLine();Console.WriteLine();}static async Task doSomething(CancellationToken cancellationToken){try{ int x = new Random().Next(4, 7);Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} 大約需要{x}秒才能執行結束");//模擬耗時操作await Task.Delay(x * 1000, cancellationToken); Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} 執行doSomething結束");}catch (OperationCanceledException ex){//如果被取消,則拋出異常Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} 超時了:" + ex.Message);}}
二?
????????如果我們要設置超時的方法本身不支持異步或超時參數時,可以通過使用Task
和CancellationToken
來實現,但這需要一些間接的方式。? ? ? ?
????????假設,方法doOperate(定義如下)執行時間較長,我們要在調用它時,設置超時操作。
static bool doOperate(int x, string y, out string error) { // 模擬長時間操作 System.Threading.Thread.Sleep(5000); // 假設操作需要5秒 error = null; // 假設沒有錯誤 Console.WriteLine($"Hello {y}");return true; // 假設操作成功 }
????????從doOperate的定義中,我們可以看到:doOperate方法并沒有設計為異步且不接受超時或取消令牌。
? ? ? ? 這種情況下,如果我們想設置在調用doOperate方法時超時,一種常用的方法是將doOperate
的調用放在一個單獨的任務中,并使用Task.Delay
和Task.WhenAny
來等待doOperate
或超時時間結束。
????????這里我們可以使用Task.Run
來封裝doOperate
的調用。
????????但請注意,這可能會引入線程池的使用,如果doOperate
是CPU密集型的操作,可能會影響系統性能。如果doOperate
主要是IO操作(比如文件訪問或數據庫查詢),這種方法的影響會比較小。
static void Main(string[] args){MethodX();}static void MethodX(){string error = null;bool result = CallDoOperateWithTimeout(3*1000, out error);if (!result){Console.WriteLine($"Operation failed: {error ?? "Timeout occurred."}");}else{Console.WriteLine("Operation completed successfully.");}}static bool CallDoOperateWithTimeout(int timeoutMilliseconds, out string error){var cts = new CancellationTokenSource(timeoutMilliseconds);string tempError = null;bool iResult = false;Task task = Task.Run(() =>{try{doOperate(42, "someInput", out tempError);iResult = true;}catch (Exception ex){tempError = ex.Message;iResult = false;}}, cts.Token);try{task.Wait(cts.Token); // 等待任務完成或拋出異常 error = tempError;return iResult;}catch (OperationCanceledException){error = "Operation timed out.";return false;}catch (Exception ex){error = ex.Message;return false;}}
????????執行后,我們會發現,確實提示”Operation failed: Operation timed out.“,但是也打印了”Hello somInput“——即doOperate
方法并沒有被終止掉,這是因為:
????????doOperate
?方法本身并不是真正的異步方法(即,它并沒有使用?async
?關鍵字,也沒有?await
?任何異步操作)。相反,它使用了?Thread.Sleep
?來模擬長時間的操作,這是一個阻塞調用,會阻塞執行它的線程直到指定的時間過去。
????????當我們從?Main
?方法或任何其他同步上下文中啟動這個?Task
?時,雖然?Task
?本身是在一個單獨的線程上執行的,但?doOperate
?方法內的?Thread.Sleep
?會阻塞那個單獨的線程。與此同時,Main
?方法中的代碼會繼續執行到?task.Wait(cts.Token)
,它等待?Task
?完成或超時。
????????如果?Task
(即?doOperate
?方法的執行)在超時之前還沒有完成(在這個例子中是5秒),CancellationTokenSource
?的?Token
?將會被觸發來請求取消操作。但是,請注意,doOperate
?方法內部并沒有檢查?CancellationToken
?的狀態,因此它不會提前退出。因此,Thread.Sleep
?將會繼續執行直到其完成,隨后?doOperate
?方法將輸出?"Hello {y}"
?并返回?true
。
????????然而,在?Main
?方法中,由于已經超過了超時時間,task.Wait(cts.Token)
?會拋出一個?OperationCanceledException
(或者,如果?Task
?實際上在超時之后完成了,則不會拋出異常,但在這個例子中它不會)。這個異常會被捕獲,并且錯誤消息會被設置為?"Operation timed out."
。
????????要解決這個問題并讓?doOperate
?方法能夠響應取消請求,您需要在?doOperate
?方法內部定期檢查?CancellationToken
?的狀態。
????????但是,由于?doOperate
?使用了?Thread.Sleep
,而?Thread.Sleep
?是不支持取消的,您需要使用其他方法來模擬異步操作,比如使用?Task.Delay
(它可以接受一個?CancellationToken
)或者實現您自己的異步邏輯(比如輪詢某種條件或等待某個事件)。
????????然而,在這個特定的例子中,由于?doOperate
?方法是同步的并且使用了?Thread.Sleep
,所以我們無法直接讓它響應取消請求。
????????如果我們想要讓?doOperate
?能夠被取消,我們需要重寫doOperate
?以使用異步模式,或者找到一種方法來避免在需要響應取消的場景中使用?Thread.Sleep
。
????????如果我們只是想在超時后停止等待?doOperate
?的結果,并且不關心?doOperate
?方法是否實際完成,那么我們的代碼已經按預期工作了,只是?doOperate
?會在后臺繼續執行直到完成。如果我們想要確保?doOperate
?在超時后被取消(即停止執行),那么我需要重新設計?doOperate
?方法以支持取消。
三
????????重寫doOperate
?方法,將它轉變為一個異步方法,并使用?CancellationToken
?來檢查是否應該提前退出。
????????然而,由于原始的?doOperate
?方法使用了?Thread.Sleep
?來模擬長時間操作,這是不可取消的,我們需要找到一個替代方案。
一個常見的替代方案是使用?Task.Delay
,它是一個可取消的異步延時操作。下面是重寫后的?doOperate
?方法和相應的調用邏輯:
/// <summary>/// 封裝操作結果的類/// </summary>private class OperationResult{/// <summary>/// 執行成功/// </summary>public bool Success { get; set; }/// <summary>/// 執行發生異常時的錯誤消息/// </summary>public string Error { get; set; }}/// <summary>/// 異步版本的doOperate方法/// </summary>/// <param name="x"></param>/// <param name="y"></param>/// <param name="cancellationToken"></param>/// <returns></returns>static async Task<OperationResult> doOperateAsync(int x, string y, CancellationToken cancellationToken){try{// 使用Task.Delay模擬異步操作,該操作支持取消 await Task.Delay(5000, cancellationToken);// 假設這是耗時的異步操作 Console.WriteLine($"Hello {y}");return new OperationResult { Success = true, Error = null };}catch (OperationCanceledException){// 如果操作被取消 return new OperationResult { Success = false, Error = "Operation was cancelled." };}catch (Exception ex){// 捕獲并處理其他可能的異常 return new OperationResult { Success = false, Error = ex.Message };}}
static async Task Main(string[] args){await MethodXAsync();Console.WriteLine("press any key to end..."); Console.ReadKey();}static async Task MethodXAsync(){//設置超時時間是3秒OperationResult result = await CallDoOperateWithTimeoutAsync(3 * 1000);if (!result.Success){Console.WriteLine($"Operation failed: {result.Error ?? "Timeout occurred."}");}else{Console.WriteLine("Operation completed successfully.");}}static async Task<OperationResult> CallDoOperateWithTimeoutAsync(int timeoutMilliseconds){var cts = new CancellationTokenSource(timeoutMilliseconds);try{// 注意:這里我們不需要將cts.Token傳遞給Task.Run, // 因為我們是在等待DoOperateAsync的完成,而不是Task.Run的完成。 // Task.Run主要用于在后臺線程上執行代碼,但在這里我們直接調用異步方法。 var result = await doOperateAsync(42, "someInput", cts.Token);return result;}catch (TaskCanceledException){return new OperationResult { Success = false, Error = "Operation timed out." };}catch (Exception ex){return new OperationResult { Success = false, Error = ex.Message };}}
請注意以下幾點:
-
我將?
Main
?方法改為異步的,并使用了?await
?關鍵字來等待?MethodXAsync
?的完成。這是處理異步程序的常見做法。 -
CallDoOperateWithTimeoutAsync
?方法直接調用?doOperateAsync
?并等待其完成,同時處理可能的取消異常和其他異常。doOperateAsync
?方法現在是一個返回?OperationResult
?實例的異步方法,該實例包含了操作的成功狀態和可能的錯誤消息。 -
DoOperateAsync
?方法現在是一個異步方法,可以在不阻塞當前線程的情況下執行長時間的操作。
如果?doOperateAsync
?中的代碼需要在另一個線程上執行(例如,因為它執行了阻塞的 I/O 操作),那么我們可以考慮使用?Task.Run
?來封裝這部分代碼。但是,在這個例子中,Task.Delay
?已經是一個異步操作,所以我們不需要額外的線程。 -
我創建了一個?
OperationResult
?類來封裝?doOperate
?方法的成功狀態和可能的錯誤消息。這樣,我們就可以在異步操作完成后返回一個包含這些信息的單一對象。
????????現在,當我們運行這個程序時,如果?doOperateAsync
?方法在超時之前完成,它將輸出?"Hello someInput"
?并報告成功。如果超時發生,它將報告超時錯誤,并且?doOperateAsync
?方法中的?Console.WriteLine
?將不會被執行(因為?Task.Delay
?會被取消)。?