簡體   English   中英

取消不接受CancellationToken的異步操作的正確方法是什么?

[英]What is the correct way to cancel an async operation that doesn't accept a CancellationToken?

取消以下內容的正確方法是什么?

var tcpListener = new TcpListener(connection);
tcpListener.Start();
var client = await tcpListener.AcceptTcpClientAsync();

簡單地調用tcpListener.Stop()似乎會導致ObjectDisposedException ,並且AcceptTcpClientAsync方法不接受CancellationToken結構。

我是否完全缺少明顯的東西?

假設您不想在TcpListener上調用Stop方法 ,那么這里沒有完美的解決方案。

如果您可以在某個時間段內未完成操作時收到通知,但允許原始操作完成,那么可以創建一個擴展方法,如下所示:

public static async Task<T> WithWaitCancellation<T>( 
    this Task<T> task, CancellationToken cancellationToken) 
{
    // The tasck completion source. 
    var tcs = new TaskCompletionSource<bool>(); 

    // Register with the cancellation token.
    using(cancellationToken.Register( s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs) ) 
    {
        // If the task waited on is the cancellation token...
        if (task != await Task.WhenAny(task, tcs.Task)) 
            throw new OperationCanceledException(cancellationToken); 
    }

    // Wait for one or the other to complete.
    return await task; 
}

以上摘自Stephen Toub的博客文章“如何取消不可取消的異步操作?”

這里需要說明的值得重復,這實際上並沒有取消操作,因為沒有了過載AcceptTcpClientAsync方法 ,需要一個CancellationToken ,它不能被取消。

這意味着,如果擴展方法指示確實發生了取消操作,那么您將取消對原始Task的回調的等待, 而不是取消操作本身。

為此,這就是為什么我將方法從WithCancellation重命名為WithCancellationWithWaitCancellation以指示您要取消的是wait ,而不是實際的操作。

從那里,很容易在您的代碼中使用:

// Create the listener.
var tcpListener = new TcpListener(connection);

// Start.
tcpListener.Start();

// The CancellationToken.
var cancellationToken = ...;

// Have to wait on an OperationCanceledException
// to see if it was cancelled.
try
{
    // Wait for the client, with the ability to cancel
    // the *wait*.
    var client = await tcpListener.AcceptTcpClientAsync().
        WithWaitCancellation(cancellationToken);
}
catch (AggregateException ae)
{
    // Async exceptions are wrapped in
    // an AggregateException, so you have to
    // look here as well.
}
catch (OperationCancelledException oce)
{
    // The operation was cancelled, branch
    // code here.
}

請注意,必須取消對客戶端的調用,以捕獲在取消等待后拋出的OperationCanceledException實例。

我還拋出了AggregateException異常,因為從異步操作拋出異常時包裝了異常(在這種情況下,您應該自己進行測試)。

面對諸如Stop方法之類的方法 (基本上,不管發生了什么事情,它都會劇烈地破壞一切),哪種方法才是更好的方法,這當然取決於您的情況。

如果您不共享正在等待的資源(在本例中為TcpListener ),則可能更好地利用資源來調用abort方法並吞下來自您正在等待的操作的任何異常(調用stop時必須翻轉一下,並在其他等待操作的區域中監視該位)。 這給代碼增加了一些復雜性,但是如果您擔心資源利用和盡快清理,並且可以使用此選擇,那么這就是要走的路。

如果資源利用率不是問題,並且您對更協作的機制感到滿意,並且您共享資源,那么使用WithWaitCancellation方法就可以了。 這里的優點是它的代碼更簡潔,更易於維護。

盡管casperOne的答案是正確的,但對於實現相同目標的WithCancellation (或WithWaitCancellation )擴展方法,存在一種更干凈的潛在實現:

static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    return task.IsCompleted
        ? task
        : task.ContinueWith(
            completedTask => completedTask.GetAwaiter().GetResult(),
            cancellationToken,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
}
  • 首先,我們通過檢查任務是否已經完成來進行快速路徑優化。
  • 然后,我們只需注冊到原始任務的延續,然后傳遞CancellationToken參數。
  • 繼續操作在可能的情況下( TaskContinuationOptions.ExecuteSynchronously )同步地提取原始任務的結果(如果有的話,則異常),如果沒有,則使用ThreadPool線程( TaskScheduler.Default ),同時觀察CancellationToken是否取消。

如果原始任務在CancellationToken之前完成,則返回的任務將存儲結果,否則該任務將被取消並在等待時拋出TaskCancelledException

暫無
暫無

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

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