歡迎光臨
每天分享高質量文章

你不一定知曉的C#取消非同步操作

作者:Julian_醬

連結:http://www.cnblogs.com/mi12205599/p/10572840.html

 

在.NET和C#中執行非同步程式碼相當簡單,因為我們有時候需要取消正在進行的非同步操作,透過本文可以掌握 透過CancellationToken取消任務(包括non-cancellable任務)。

早期

早期.NET使用 BackgroundWorker 完成非同步長時間執行操作。

 

可以使用CacnelAsync方法設定 CancellationPending = true

 

private void BackgroundLongRunningTask(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = (BackgroundWorker)sender;

    for (int i = 1; i <= 10000; i++)
    {
        if (worker.CancellationPending == true)
        {
            e.Cancel = true;
            break;
        }
        
        // Do something
    }
}

 

已經不再推薦這種方式來完成非同步和長時間執行的操作,但是大部分概念在現在依舊可以使用。

Task橫空出世

Task代表一個非同步操作,該類表示一個非同步不傳回值的操作, 泛型版本Task表示非同步有傳回值的操作。

 

可使用async/await 語法糖程式碼去完成非同步操作。

 

以下建立一個簡單的長時間執行的操作:

 

/// 
/// Compute a value for a long time.
/// 

/// The value computed.
/// Number of iterations to do.
private static Task<decimal> LongRunningOperation(int loop)
{
// Start a task and return it
return Task.Run(() =>
{
decimal result = 0;

// Loop for a defined number of iterations
for (int i = 0; i < loop; i++)
{
// Do something that takes times like a Thread.Sleep in .NET Core 2.
Thread.Sleep(10);
result += i;
}

return result;
});
}
// 這裡我們使用Thread.Sleep 模仿長時間執行的操作

 

簡單非同步呼叫程式碼:

 

public static async Task ExecuteTaskAsync()
{
    Console.WriteLine(nameof(ExecuteTaskAsync));
    Console.WriteLine("Result {0}", await LongRunningOperation(100));
    Console.WriteLine("Press enter to continue");
    Console.ReadLine();
}

敲黑板: C#取消非同步操作分為

① 讓程式碼可取消(Cancellable)

 

因為一些原因,長時間執行的操作花費了 冗長的時間(需要取消,避免佔用資源);或者不願意再等待執行結果了我們會取消非同步操作。

 

為完成目的需要在 長時間執行的非同步任務中傳入CancellationToken:

 

/// 
/// Compute a value for a long time.
/// 

/// The value computed.
/// Number of iterations to do.
/// The cancellation token.
private static Task<decimal> LongRunningCancellableOperation(int loop, CancellationToken cancellationToken)
{
Task<decimal> task = null;

// Start a task and return it
task = Task.Run(() =>
{
decimal result = 0;

// Loop for a defined number of iterations
for (int i = 0; i < loop; i++)
{
// Check if a cancellation is requested, if yes,
// throw a TaskCanceledException.

if (cancellationToken.IsCancellationRequested)
throw new TaskCanceledException(task);

// Do something that takes times like a Thread.Sleep in .NET Core 2.
Thread.Sleep(10);
result += i;
}

return result;
});
return task;
}

 

在長時間執行的操作中監測 IsCancellationRequested方法 (當前是否發生取消命令),這裡我傾向去包裝一個TaskCanceledException異常類(給上層方法呼叫者更多處理的可能性); 當然可以呼叫ThrowIfCancellationRequested方法丟擲OperationCanceledException異常。

 

② 觸發取消命令

 

CancellationToken結構體相當於打入在非同步操作內部的楔子,隨時等候後方發來的取消命令。

 

操縱以上CancellationToken狀態的物件是 CancellationTokenSource,這個物件是取消操作的命令釋出者。

 

預設的建構式就支援了 超時取消:

 

//  以下程式碼 利用 CancellationSource預設建構式 完成超時取消
public static async Task ExecuteTaskWithTimeoutAsync(TimeSpan timeSpan)
{
    Console.WriteLine(nameof(ExecuteTaskWithTimeoutAsync));
    using (var cancellationTokenSource = new CancellationTokenSource(timeSpan))
    {
        try
        {
            var result = await LongRunningCancellableOperation(500, cancellationTokenSource.Token);
            Console.WriteLine("Result {0}", result);
        }
        catch (TaskCanceledException)
        {
            Console.WriteLine("Task was cancelled");
        }
    }
    Console.WriteLine("Press enter to continue");
    Console.ReadLine();
}

 

附①: 高階操作,完成手動取消:

 

自然我們關註到 CancellationSource 的幾個方法, 要想在非同步操作的時候 手動取消操作,需要建立另外的執行緒 等待手動取消操作的指令。

 

public static async Task ExecuteManuallyCancellableTaskAsync()
{
    Console.WriteLine(nameof(ExecuteManuallyCancellableTaskAsync));

    using (var cancellationTokenSource = new CancellationTokenSource())
    {
        // Creating a task to listen to keyboard key press
        var keyBoardTask = Task.Run(() =>
        {
            Console.WriteLine("Press enter to cancel");
            Console.ReadKey();

            // Cancel the task
            cancellationTokenSource.Cancel();
        });

        try
        {
            var longRunningTask = LongRunningCancellableOperation(500, cancellationTokenSource.Token);

            var result = await longRunningTask;
            Console.WriteLine("Result {0}", result);
            Console.WriteLine("Press enter to continue");
        }
        catch (TaskCanceledException)
        {
            Console.WriteLine("Task was cancelled");
        }

        await keyBoardTask;
    }
}
// 以上是一個控制檯程式,非同步接收控制檯輸入,發出取消命令。

附②:高階操作,取消 non-Cancellable任務 :

 

有時候,非同步操作程式碼並不提供 對 Cancellation的支援,也就是以上長時間執行的非同步操作

 

LongRunningCancellableOperation(int loop, CancellationToken cancellationToken) 並不提供引數2的傳入,相當於不允許 打入楔子。

 

這時我們怎樣取消 這樣的non-Cancellable 任務?

 

可考慮利用 Task.WhenAny( params tasks) 操作曲線取消:

 

  • 利用TaskCompletionSource 註冊非同步可取消任務

  • 等待待non-cancellable 操作和以上建立的 非同步取消操作

 

private static async Task<decimal> LongRunningOperationWithCancellationTokenAsync(int loop, CancellationToken cancellationToken)
{
    // We create a TaskCompletionSource of decimal
    var taskCompletionSource = new TaskCompletionSource<decimal>();

    // Registering a lambda into the cancellationToken
    cancellationToken.Register(() =>
    {
        // We received a cancellation message, cancel the TaskCompletionSource.Task
        taskCompletionSource.TrySetCanceled();
    });

    var task = LongRunningOperation(loop);

    // Wait for the first task to finish among the two
    var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);

    return await completedTask;
}

 

像上面程式碼一樣執行取消命令 :

 

public static async Task CancelANonCancellableTaskAsync()
{
    Console.WriteLine(nameof(CancelANonCancellableTaskAsync));

    using (var cancellationTokenSource = new CancellationTokenSource())
    {
        // Listening to key press to cancel
        var keyBoardTask = Task.Run(() =>
        {
            Console.WriteLine("Press enter to cancel");
            Console.ReadKey();

            // Sending the cancellation message
            cancellationTokenSource.Cancel();
        });
        try
        {
            // Running the long running task
            var longRunningTask = LongRunningOperationWithCancellationTokenAsync(100, cancellationTokenSource.Token);
            var result = await longRunningTask;

            Console.WriteLine("Result {0}", result);
            Console.WriteLine("Press enter to continue");
        }
        catch (TaskCanceledException)
        {
            Console.WriteLine("Task was cancelled");
        }

        await keyBoardTask;
    }
}

總結

大多數情況下,我們不需要編寫自定義可取消任務,因為我們只需要使用現有API。但要知道它是如何在幕後工作總是好的。

贊(2)

分享創造快樂