最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C#使用Cancellation取消異步任務(wù)的全過程

 更新時間:2026年01月29日 09:12:39   作者:xb1132  
在開發(fā)應(yīng)用程序時,我們經(jīng)常會遇到一些需要長時間運行的操作,比如:從網(wǎng)絡(luò)下載大文件、處理大量數(shù)據(jù)、執(zhí)行復(fù)雜的計算任務(wù)等,這些操作可能需要幾秒、幾分鐘甚至更長時間才能完成,為了安全地取消操作,.NET提供了CancellationToken機制,本文給大家詳細介紹一下

一、什么是 CancellationToken?

1.1 為什么要學(xué)習(xí) CancellationToken?

在開發(fā)應(yīng)用程序時,我們經(jīng)常會遇到一些需要長時間運行的操作,比如:

  • 從網(wǎng)絡(luò)下載大文件
  • 處理大量數(shù)據(jù)
  • 在運動控制系統(tǒng)中,電機從當(dāng)前位置運動到目標位置
  • 執(zhí)行復(fù)雜的計算任務(wù)

這些操作可能需要幾秒、幾分鐘甚至更長時間才能完成。但是如果用戶想中途取消這個操作,或者發(fā)生了緊急情況需要立即停止,我們應(yīng)該怎么做呢?

在傳統(tǒng)的編程方式中,如果我們想停止一個正在運行的線程,可能會使用 Thread.Abort() 這樣的方法。但這種方式非常危險,因為它會強制終止線程,可能會導(dǎo)致:

  • 數(shù)據(jù)不一致(線程正在修改數(shù)據(jù)時被突然打斷)
  • 資源泄漏(文件、數(shù)據(jù)庫連接等沒有正確關(guān)閉)
  • 程序狀態(tài)不可預(yù)測

為了安全地取消操作,.NET 提供了 CancellationToken 機制。這是一種協(xié)作式的取消方式,不是強制終止,而是通知操作"我想取消你,請你自己安全地停止"。

1.2 CancellationToken 的工作原理

想象一下這樣的場景:

傳統(tǒng)方式(不推薦):

老板 → 員工正在工作 → 突然拔掉電源(暴力終止)
結(jié)果:員工可能正在處理重要文件,直接斷電導(dǎo)致數(shù)據(jù)損壞

CancellationToken 方式(推薦):

老板 → 員工正在工作 → 老板說"請停止工作"
員工 → 檢查到停止信號 → 保存當(dāng)前工作 → 清理現(xiàn)場 → 安全停止
結(jié)果:工作有序停止,沒有數(shù)據(jù)損壞

在 CancellationToken 機制中:

  • CancellationTokenSource(令牌源):相當(dāng)于"老板",負責(zé)發(fā)起取消請求
  • CancellationToken(令牌):相當(dāng)于"通知",傳遞給執(zhí)行任務(wù)的代碼,告訴它是否應(yīng)該停止
  • 執(zhí)行任務(wù)的代碼:相當(dāng)于"員工",定期檢查令牌狀態(tài),如果收到取消信號就安全地停止

1.3 核心概念詳解

CancellationTokenSource(令牌源)

  • 作用:這是取消操作的源頭,只有通過它才能發(fā)起取消請求
  • 特點:可以主動調(diào)用 Cancel() 方法來取消操作
  • 生命周期:使用后應(yīng)該及時釋放資源(實現(xiàn)了 IDisposable 接口)
  • 比喻:就像一個廣播站,只有它能發(fā)送"取消"的廣播信號

CancellationToken(令牌)

  • 作用:被傳遞給異步方法,用于檢查是否收到取消信號
  • 特點:本身不能主動取消,只能被動接收取消狀態(tài)
  • 不可變性:一旦創(chuàng)建就不能修改,這是線程安全的
  • 比喻:就像一個收音機,只能接收信號,不能發(fā)送信號

CancellationTokenRegistration(取消注冊)

  • 作用:用于注冊取消時的回調(diào)函數(shù)
  • 特點:當(dāng)取消發(fā)生時,會自動執(zhí)行注冊的回調(diào)
  • 比喻:就像設(shè)置了鬧鐘,當(dāng)"取消"信號到來時自動執(zhí)行某些操作

1.4 CancellationToken 的使用場景

在運動控制卡編程中,CancellationToken 尤其重要:

  1. 緊急停止:操作員按下急停按鈕,需要立即停止所有電機運動
  2. 用戶中斷:用戶點擊"取消"按鈕,停止當(dāng)前的運動任務(wù)
  3. 超時保護:運動超過預(yù)設(shè)時間仍未到達目標位置,自動停止
  4. 多軸同步停止:多軸聯(lián)動時,一個軸異常需要停止所有軸
  5. 資源清理:取消運動時,需要關(guān)閉相關(guān)硬件設(shè)備,釋放資源

沒有 CancellationToken,我們很難實現(xiàn)這些功能,或者實現(xiàn)的代碼會很復(fù)雜且不安全。

二、基本使用

2.1 創(chuàng)建和使用 CancellationToken

讓我們通過一個完整的例子來理解如何使用 CancellationToken。這個例子模擬了一個需要 10 秒才能完成的工作,但我們在 5 秒后就想取消它。

CancellationTokenSource cts = new CancellationTokenSource();

CancellationToken token = cts.Token;

async Task DoWorkAsync(CancellationToken ct)
{
    for(int i=0;i<10;i++)
    {
        ct.ThrowIfCancellationRequested();//檢查請求是否取消
        Console.WriteLine($"working...{i+1}/10");
        await Task.Delay(1000,ct);
    }
    Console.WriteLine("work end!");
}

var task = DoWorkAsync(token);

await Task.Delay(5000);
cts.Cancel();

try
{
    await task;
}
catch(OperationCanceledException)
{
    Console.WriteLine("wrok stop");
}

關(guān)鍵要點:

  • ThrowIfCancellationRequested() 是主動檢查,如果不調(diào)用它,任務(wù)可能不會及時響應(yīng)取消
  • 傳遞 token 給 Task.Delay() 可以在等待期間響應(yīng)取消
  • 取消通過拋出異常來實現(xiàn),這是 .NET 的標準做法
  • OperationCanceledException 是預(yù)期的異常,不是程序錯誤

2.2 CancellationTokenSource 的常見方法

CancellationTokenSource 提供了多個有用的方法,讓我們來看看它們的作用和使用場景。

// 創(chuàng)建一個 CancellationTokenSource
CancellationTokenSource cts = new CancellationTokenSource();

// 方法1:立即取消
// 這是最常用的方法,立即發(fā)出取消信號
// 通常在用戶點擊"取消"按鈕或發(fā)生錯誤時使用
cts.Cancel();
Console.WriteLine("已發(fā)出取消信號");

// 方法2:延遲后自動取消
// 方式A:使用 TimeSpan,更易讀
cts.CancelAfter(TimeSpan.FromSeconds(5));
// 5秒后會自動發(fā)出取消信號,無需手動調(diào)用 Cancel()

// 方式B:使用毫秒數(shù),更精確
cts.CancelAfter(5000);
// 5000毫秒(5秒)后自動取消

// 應(yīng)用場景:防止任務(wù)長時間卡死
// 例如:網(wǎng)絡(luò)請求最多等待10秒,超時自動取消
// 例如:電機運動最多等待30秒到達目標位置,超時自動停止

// 方法3:創(chuàng)建關(guān)聯(lián)多個 token 的新 token
// 這是高級用法,允許同時響應(yīng)多個取消源
var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();

// 創(chuàng)建一個關(guān)聯(lián)的 token
// 只要 cts1 或 cts2 中任一被取消,linkedCts.Token 都會收到取消信號
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
    cts1.Token, cts2.Token);

// 應(yīng)用場景:
// 1. 全局取消 + 用戶取消
//    cts1.Token:用戶點擊取消按鈕
//    cts2.Token:系統(tǒng)關(guān)閉時自動取消
//    linkedCts.Token:同時響應(yīng)這兩種取消

// 2. 運動控制場景
//    cts1.Token:用戶點擊"停止"按鈕
//    cts2.Token:限位開關(guān)觸發(fā)(硬件信號)
//    linkedCts.Token:任一情況發(fā)生都停止運動

// 方法4:釋放資源
// CancellationTokenSource 實現(xiàn)了 IDisposable 接口
// 使用完畢后應(yīng)該調(diào)用 Dispose() 釋放資源
cts.Dispose();

// 推薦做法:使用 using 語句自動釋放
using var ctsAuto = new CancellationTokenSource();
// 使用 ctsAuto
// 離開作用域時自動調(diào)用 Dispose()

詳細說明 CancelAfter 的使用:

CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(3));
var token = cts.Token;

try
{
    await Task.Delay(5000,token);
    Console.WriteLine("Work end");
}
catch(OperationCanceledException)
{
    if(token.IsCancellationRequested)
        Console.WriteLine("work stop");
    else
        Console.WriteLine("work cancel");
}

詳細說明關(guān)聯(lián) Token 的使用:

// 場景:運動控制系統(tǒng),需要同時響應(yīng)多個停止信號
class MotionSystem
{
    private CancellationTokenSource _userCancelSource; // 用戶取消
    private CancellationTokenSource _emergencyStopSource; // 急停信號
    private CancellationTokenSource _linkedSource; // 關(guān)聯(lián)的源

    public void Start()
    {
        _userCancelSource = new CancellationTokenSource();
        _emergencyStopSource = new CancellationTokenSource();

        // 創(chuàng)建關(guān)聯(lián)的 token,任一信號都觸發(fā)取消
        _linkedSource = CancellationTokenSource.CreateLinkedTokenSource(
            _userCancelSource.Token,
            _emergencyStopSource.Token
        );

        // 使用關(guān)聯(lián)的 token 啟動運動
        StartMotionAsync(_linkedSource.Token);
    }

    // 用戶點擊"停止"按鈕
    public void UserStop()
    {
        _userCancelSource?.Cancel();
        Console.WriteLine("用戶請求停止");
    }

    // 硬件急停信號觸發(fā)
    public void EmergencyStop()
    {
        _emergencyStopSource?.Cancel();
        Console.WriteLine("急停信號觸發(fā)!");
    }
}

三、在異步方法中使用 CancellationToken

3.1 異步方法接收 CancellationToken

在 .NET 的異步編程最佳實踐中,所有可能長時間運行的異步方法都應(yīng)該接受 CancellationToken 參數(shù)。這是一個約定,也是編寫健壯代碼的關(guān)鍵。

// 好的做法:所有異步方法都應(yīng)接受 CancellationToken
// 參數(shù)命名約定:通常使用 ct 或 cancellationToken
// 參數(shù)位置約定:通常是最后一個參數(shù)
// 默認值:使用 default(CancellationToken),允許調(diào)用方選擇不傳遞
async Task DownloadFileAsync(string url, CancellationToken ct = default)
{
    try
    {
        // 在耗時操作中傳遞 token
        // 這樣可以讓操作本身支持取消
        using var httpClient = new HttpClient();

        // GetAsync 方法支持 CancellationToken
        // 如果在等待響應(yīng)時收到取消信號,會立即取消請求
        var response = await httpClient.GetAsync(url, ct);

        // ReadAsStringAsync 也支持 CancellationToken
        // 如果在讀取內(nèi)容時收到取消信號,會立即停止讀取
        var content = await response.Content.ReadAsStringAsync(ct);

        Console.WriteLine($"下載完成,內(nèi)容長度: {content.Length}");
    }
    catch (OperationCanceledException)
    {
        // 捕獲取消異常,這是正常的取消流程
        // 不應(yīng)該記錄為錯誤
        Console.WriteLine("下載已取消");
    }
    catch (HttpRequestException ex)
    {
        // 捕獲網(wǎng)絡(luò)錯誤,這是真正的錯誤
        Console.WriteLine($"網(wǎng)絡(luò)錯誤: {ex.Message}");
    }
}

為什么要在參數(shù)中傳遞 CancellationToken?

  1. 允許調(diào)用方控制取消:調(diào)用方可以決定是否允許取消,以及何時取消
  2. 遵循.NET慣例:這是.NET異步編程的標準做法
  3. 支持取消傳播:允許取消信號從上層方法傳遞到下層方法
  4. 提高響應(yīng)性:讓整個調(diào)用鏈都能快速響應(yīng)取消請求

3.2 檢查取消狀態(tài)的兩種方式

在異步方法中,有兩種方式來檢查是否應(yīng)該取消操作。理解它們的區(qū)別很重要,因為它們適用于不同的場景。

方式1:ThrowIfCancellationRequested() - 拋出異常

async Task ProcessDataAsync(CancellationToken ct)
{
    for (int i = 0; i < 1000; i++)
    {
        // 方式1:檢查并拋出異常
        // 如果已請求取消,拋出 OperationCanceledException
        // 優(yōu)點:代碼簡潔,自動停止執(zhí)行
        // 缺點:需要使用 try-catch 處理異常
        ct.ThrowIfCancellationRequested();

        // 執(zhí)行一些工作
        await DoSomeWorkAsync(i, ct);
    }
}

工作原理:

  1. 調(diào)用 ThrowIfCancellationRequested() 檢查 token 的狀態(tài)
  2. 如果 IsCancellationRequestedtrue,拋出 OperationCanceledException
  3. 拋出異常后,后續(xù)代碼不會執(zhí)行,方法立即退出
  4. 異常會向上傳播,直到被某個 try-catch 捕獲

適用場景:

  • 想要立即停止操作,不需要做任何清理工作
  • 希望取消信號以異常的形式向上傳播
  • 方法調(diào)用鏈中已經(jīng)有人處理 OperationCanceledException

方式2:IsCancellationRequested - 自行處理

async Task ProcessDataAsync(CancellationToken ct)
{
    List<int> processedData = new List<int>();

    for (int i = 0; i < 1000; i++)
    {
        // 方式2:檢查后自行處理
        // 只檢查狀態(tài),不拋出異常
        // 優(yōu)點:可以執(zhí)行清理邏輯,控制退出方式
        // 缺點:需要自己編寫退出邏輯
        if (ct.IsCancellationRequested)
        {
            Console.WriteLine("檢測到取消請求,開始清理資源...");

            // 可以在這里執(zhí)行清理工作
            // 例如:保存已處理的數(shù)據(jù)、關(guān)閉連接、釋放資源等
            await SavePartialResultsAsync(processedData);

            // 優(yōu)雅地退出方法
            return; // 正常返回,不拋出異常
        }

        // 執(zhí)行工作
        int result = await DoSomeWorkAsync(i, ct);
        processedData.Add(result);
    }
}

工作原理:

  1. 檢查 IsCancellationRequested 屬性
  2. 如果為 true,執(zhí)行自定義的清理邏輯
  3. 使用 return 正常退出方法,不拋出異常
  4. 調(diào)用方不知道方法是被取消的,只認為方法正常完成

適用場景:

  • 需要在取消時執(zhí)行特定的清理工作
  • 希望部分完成的結(jié)果能夠被保存
  • 不希望以異常的形式通知調(diào)用方
  • 需要記錄取消事件

兩種方式的對比

特性ThrowIfCancellationRequested()IsCancellationRequested
是否拋出異常
代碼簡潔度中等
能否執(zhí)行清理邏輯只能在 finally 塊中可以在檢測后立即執(zhí)行
調(diào)用方是否知道取消是(通過異常)否(正常返回)
推薦使用場景大多數(shù)情況需要特殊清理邏輯時

實際應(yīng)用示例

讓我們通過一個實際的運動控制例子來理解兩種方式的區(qū)別:

// 場景1:簡單的數(shù)據(jù)處理,不需要清理
async Task SimpleProcessAsync(CancellationToken ct)
{
    for (int i = 0; i < 1000; i++)
    {
        ct.ThrowIfCancellationRequested(); // ? 使用 ThrowIfCancellationRequested
        await ProcessItemAsync(i, ct);
    }
}

// 場景2:需要保存中間結(jié)果
async Task ProcessWithSavingAsync(CancellationToken ct)
{
    var results = new List<int>();

    for (int i = 0; i < 1000; i++)
    {
        if (ct.IsCancellationRequested) // ? 使用 IsCancellationRequested
        {
            await SavePartialResultsAsync(results); // 保存已完成的部分
            return;
        }

        int result = await ProcessItemAsync(i, ct);
        results.Add(result);
    }
}

// 場景3:需要通知調(diào)用方取消狀態(tài)
async Task ProcessWithNotificationAsync(CancellationToken ct)
{
    try
    {
        for (int i = 0; i < 1000; i++)
        {
            ct.ThrowIfCancellationRequested(); // ? 使用 ThrowIfCancellationRequested
            await ProcessItemAsync(i, ct);
        }
    }
    catch (OperationCanceledException)
    {
        // 重新拋出,讓調(diào)用方知道操作被取消
        throw; // ? 拋出異常通知調(diào)用方
    }
}

// 場景4:需要同時處理取消和異常
async Task ProcessWithBothAsync(CancellationToken ct)
{
    var results = new List<int>();

    try
    {
        for (int i = 0; i < 1000; i++)
        {
            if (ct.IsCancellationRequested) // ? 使用 IsCancellationRequested
            {
                await CleanupAsync(results);
                throw new OperationCanceledException(ct); // 手動拋出異常
            }

            int result = await ProcessItemAsync(i, ct);
            results.Add(result);
        }
    }
    catch (Exception ex) when (ex is not OperationCanceledException)
    {
        // 處理其他異常
        await HandleErrorAsync(ex);
        throw;
    }
}

3.3 在 WinForms 中使用

在 Windows 窗體應(yīng)用程序中使用 CancellationToken 是非常常見的場景。這通常涉及用戶界面(UI)和后臺任務(wù)的交互。讓我們通過一個完整的例子來學(xué)習(xí)如何正確實現(xiàn)。

public partial class MainForm : Form
{
    // 字段:存儲 CancellationTokenSource 和當(dāng)前任務(wù)
    private CancellationTokenSource _cts;
    private Task _currentTask;

    public MainForm()
    {
        InitializeComponent();
        // 初始化時,取消按鈕應(yīng)該禁用
        btnCancel.Enabled = false;
    }

    // 開始按鈕點擊事件
    private async void btnStart_Click(object sender, EventArgs e)
    {
        // 步驟1:創(chuàng)建新的 CancellationTokenSource
        // 每次開始新任務(wù)時,都應(yīng)該創(chuàng)建一個新的實例
        _cts = new CancellationTokenSource();

        // 步驟2:更新 UI 狀態(tài)
        // 禁用開始按鈕,啟用取消按鈕,防止重復(fù)點擊
        btnStart.Enabled = false;
        btnCancel.Enabled = true;
        progressBar1.Value = 0;
        lblStatus.Text = "準備開始...";

        try
        {
            // 步驟3:啟動長時間運行的操作
            // 將 token 傳遞給方法,使其支持取消
            _currentTask = LongRunningOperation(_cts.Token);

            // 步驟4:等待操作完成
            // 使用 await 關(guān)鍵字,不會阻塞 UI 線程
            await _currentTask;

            // 步驟5:操作正常完成
            lblStatus.Text = "操作完成!";
            MessageBox.Show("操作完成!", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
        catch (OperationCanceledException)
        {
            // 步驟6:操作被取消
            // 這是正常情況,不是錯誤
            lblStatus.Text = "操作已取消";
            MessageBox.Show("操作已取消", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
        catch (Exception ex)
        {
            // 步驟7:處理其他異常
            lblStatus.Text = "發(fā)生錯誤";
            MessageBox.Show($"發(fā)生錯誤:{ex.Message}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
        finally
        {
            // 步驟8:清理資源,恢復(fù) UI 狀態(tài)
            // finally 塊中的代碼總是會執(zhí)行,無論是否發(fā)生異常
            btnStart.Enabled = true;
            btnCancel.Enabled = false;

            // 釋放 CancellationTokenSource
            _cts?.Dispose();
            _cts = null;
            _currentTask = null;
        }
    }

    // 取消按鈕點擊事件
    private void btnCancel_Click(object sender, EventArgs e)
    {
        // 步驟1:發(fā)出取消信號
        // 調(diào)用 Cancel() 會通知所有持有該 token 的任務(wù)
        _cts?.Cancel();

        // 步驟2:更新 UI
        // 禁用取消按鈕,防止重復(fù)點擊
        btnCancel.Enabled = false;
        lblStatus.Text = "正在取消...";
    }

    // 長時間運行的操作
    // 這個方法會在后臺線程執(zhí)行,不會阻塞 UI
    private async Task LongRunningOperation(CancellationToken ct)
    {
        // 模擬處理 100 個項目
        for (int i = 0; i < 100; i++)
        {
            // 步驟1:檢查是否請求取消
            // 這是在后臺線程執(zhí)行的,不會阻塞 UI
            ct.ThrowIfCancellationRequested();

            // 步驟2:執(zhí)行一些工作
            // 這里用 Task.Delay 模擬耗時操作
            await Task.Delay(100, ct);

            // 步驟3:更新 UI(重要?。?
            // 由于這個方法在后臺線程運行,不能直接訪問 UI 控件
            // 必須使用 Invoke 或 BeginInvoke 切換到 UI 線程
            this.Invoke((MethodInvoker)delegate
            {
                // 更新進度條
                progressBar1.Value = i + 1;

                // 更新狀態(tài)標簽
                lblStatus.Text = $"處理中... {i + 1}/100";

                // 可以添加其他 UI 更新
                lblProgress.Text = $"{i + 1}%";
            });
        }
    }
}

重要概念解釋:

1. 為什么需要字段變量?

// 使用字段而不是局部變量的原因:
private CancellationTokenSource _cts; // ? 字段,可以在多個方法中訪問

// ? 如果使用局部變量,其他方法無法訪問
private async void btnStart_Click(object sender, EventArgs e)
{
    var cts = new CancellationTokenSource(); // 局部變量
    await LongRunningOperation(cts.Token);
}

// ? btnCancel_Click 無法訪問上面的 cts
private void btnCancel_Click(object sender, EventArgs e)
{
    // cts 在這里不可見!
    // cts?.Cancel(); // 編譯錯誤
}

2. 為什么每次都要創(chuàng)建新的 CancellationTokenSource?

// ? 錯誤:重復(fù)使用同一個實例
private CancellationTokenSource _cts = new CancellationTokenSource();

private async void btnStart_Click(object sender, EventArgs e)
{
    // 如果用戶點擊兩次"開始",會出現(xiàn)問題
    // 第一次點擊已經(jīng)開始,第二次點擊又創(chuàng)建任務(wù)
    // 當(dāng)?shù)诙吸c擊后點擊"取消",兩次任務(wù)都會被取消
    _cts.Cancel(); // 這會影響所有使用過這個 token 的任務(wù)
}

// ? 正確:每次開始新任務(wù)時創(chuàng)建新實例
private async void btnStart_Click(object sender, EventArgs e)
{
    // 每次點擊"開始",都創(chuàng)建一個新的 CancellationTokenSource
    // 這樣不同的任務(wù)之間互不影響
    _cts = new CancellationTokenSource();
}

3. 為什么要使用 Invoke 更新 UI?

在 WinForms 中,有一條重要規(guī)則:只有創(chuàng)建控件的線程(UI 線程)才能訪問該控件。

private async Task LongRunningOperation(CancellationToken ct)
{
    for (int i = 0; i < 100; i++)
    {
        ct.ThrowIfCancellationRequested();
        await Task.Delay(100, ct);

        // ? 錯誤:直接訪問 UI 控件
        // 這個方法在后臺線程運行,不能直接訪問 UI 控件
        progressBar1.Value = i + 1; // 會拋出 InvalidOperationException

        // ? 正確:使用 Invoke 切換到 UI 線程
        this.Invoke((MethodInvoker)delegate
        {
            // 這段代碼會在 UI 線程執(zhí)行
            progressBar1.Value = i + 1;
            lblStatus.Text = $"處理中... {i + 1}/100";
        });
    }
}

Invoke 的工作原理:

  • Invoke 會將代碼排隊到 UI 線程的消息隊列中
  • UI 線程在處理消息時執(zhí)行這段代碼
  • Invoke 是同步的,會等待 UI 線程執(zhí)行完成
  • 還可以使用 BeginInvoke,它是異步的,不會等待

四、高級用法

4.1 注冊取消回調(diào)

有時候我們需要在取消發(fā)生時執(zhí)行一些特定的操作,比如清理資源、關(guān)閉設(shè)備連接、記錄日志等。CancellationToken 提供了 Register 方法,允許我們注冊回調(diào)函數(shù),當(dāng)取消發(fā)生時會自動執(zhí)行這些回調(diào)。

基本用法

// 創(chuàng)建 CancellationTokenSource
CancellationTokenSource cts = new CancellationTokenSource();

// 注冊取消時的回調(diào)
// Register 方法返回一個 CancellationTokenRegistration 對象
// 這個對象可以用于注銷回調(diào)
CancellationTokenRegistration registration = cts.Token.Register(() =>
{
    Console.WriteLine("執(zhí)行清理工作...");

    // 在這里執(zhí)行清理操作
    // 例如:關(guān)閉文件、釋放資源、關(guān)閉設(shè)備連接等
    CleanupResources();

    // 例如:記錄日志
    LogCancellation();
});

// 執(zhí)行一些工作
await DoSomeWorkAsync(cts.Token);

// 觸發(fā)取消
cts.Cancel();

// 手動注銷回調(diào)(如果需要)
// 如果回調(diào)已經(jīng)執(zhí)行,注銷沒有影響
registration.Dispose();

詳細說明:

1. Register 的工作原理

// Register 方法的簽名
// public CancellationTokenRegistration Register(Action callback)

// 回調(diào)函數(shù)會在以下情況被執(zhí)行:
// 1. 調(diào)用 CancellationTokenSource.Cancel()
// 2. 調(diào)用 CancellationTokenSource.CancelAfter() 超時后自動取消
// 3. 關(guān)聯(lián)的令牌源被取消(使用 CreateLinkedTokenSource 創(chuàng)建的)

// 回調(diào)函數(shù)的執(zhí)行時機:
// - 在 Cancel() 調(diào)用的線程中同步執(zhí)行
// - 如果注冊了多個回調(diào),會按照注冊順序依次執(zhí)行
// - 回調(diào)函數(shù)執(zhí)行期間,其他線程可能繼續(xù)運行

2. 注冊多個回調(diào)

CancellationTokenSource cts = new CancellationTokenSource();

// 可以注冊多個回調(diào)
var registration1 = cts.Token.Register(() =>
{
    Console.WriteLine("回調(diào)1: 關(guān)閉數(shù)據(jù)庫連接");
    CloseDatabaseConnection();
});

var registration2 = cts.Token.Register(() =>
{
    Console.WriteLine("回調(diào)2: 清理臨時文件");
    CleanTempFiles();
});

var registration3 = cts.Token.Register(() =>
{
    Console.WriteLine("回調(diào)3: 記錄取消日志");
    LogCancellation();
});

// 當(dāng)調(diào)用 Cancel() 時,所有回調(diào)都會執(zhí)行
cts.Cancel();

// 輸出:
// 回調(diào)1: 關(guān)閉數(shù)據(jù)庫連接
// 回調(diào)2: 清理臨時文件
// 回調(diào)3: 記錄取消日志

// 清理注冊對象
registration1.Dispose();
registration2.Dispose();
registration3.Dispose();

3. 回調(diào)的執(zhí)行順序

CancellationTokenSource cts = new CancellationTokenSource();

// 注冊回調(diào)的順序就是執(zhí)行順序
cts.Token.Register(() => Console.WriteLine("第1個回調(diào)"));
cts.Token.Register(() => Console.WriteLine("第2個回調(diào)"));
cts.Token.Register(() => Console.WriteLine("第3個回調(diào)"));

cts.Cancel();

// 輸出:
// 第1個回調(diào)
// 第2個回調(diào)
// 第3個回調(diào)

4. 回調(diào)中拋出異常

CancellationTokenSource cts = new CancellationTokenSource();

// 如果回調(diào)中拋出異常,會阻止后續(xù)回調(diào)執(zhí)行
cts.Token.Register(() =>
{
    Console.WriteLine("回調(diào)1開始");
    throw new Exception("回調(diào)1出錯");  // 拋出異常
});

cts.Token.Register(() =>
{
    Console.WriteLine("回調(diào)2");  // 不會執(zhí)行!
});

try
{
    cts.Cancel();
}
catch (AggregateException ex)
{
    Console.WriteLine($"捕獲到異常: {ex.InnerExceptions[0].Message}");
}

// 輸出:
// 回調(diào)1開始
// 捕獲到異常: 回調(diào)1出錯

可以在回調(diào)中直接catch到異常,這樣即使出現(xiàn)了異常,也不會阻止后面的回調(diào)

Register 方法的關(guān)鍵要點:

  1. 回調(diào)是同步執(zhí)行的:回調(diào)會在調(diào)用 Cancel() 的線程中同步執(zhí)行
  2. 可以注冊多個回調(diào):按照注冊順序依次執(zhí)行
  3. 應(yīng)該捕獲異常:回調(diào)中應(yīng)該捕獲異常,避免影響其他回調(diào)
  4. 可以注銷回調(diào):通過返回的 CancellationTokenRegistration 對象可以注銷回調(diào)
  5. 適合用于資源清理:在取消時自動清理資源,避免資源泄漏

4.2 傳遞 CancellationToken 到多個異步操作

在實際應(yīng)用中,我們經(jīng)常需要并行執(zhí)行多個任務(wù),并且希望能夠一次性取消所有任務(wù)。通過將同一個 CancellationToken 傳遞給所有任務(wù),可以輕松實現(xiàn)這個需求。

async Task ComplexOperationAsync(CancellationToken ct)
{
    var tasks = new List<Task>();

    // 并行執(zhí)行多個任務(wù),共享同一個取消令牌
    // 這樣,當(dāng)令牌被取消時,所有任務(wù)都會收到通知
    tasks.Add(Task.Run(() => Task1(ct), ct));
    tasks.Add(Task.Run(() => Task2(ct), ct));
    tasks.Add(Task.Run(() => Task3(ct), ct));

    try
    {
        // 等待所有任務(wù)完成
        await Task.WhenAll(tasks);
        Console.WriteLine("所有任務(wù)完成");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("某個或所有任務(wù)被取消");
        throw;
    }
    catch (AggregateException ex)
    {
        // 捕獲多個任務(wù)的異常
        Console.WriteLine($"捕獲到 {ex.InnerExceptions.Count} 個異常");
        foreach (var innerEx in ex.InnerExceptions)
        {
            Console.WriteLine($"  - {innerEx.Message}");
        }
        throw;
    }
}

// 各個任務(wù)的實現(xiàn)
async Task Task1(CancellationToken ct)
{
    for (int i = 0; i < 10; i++)
    {
        ct.ThrowIfCancellationRequested();
        Console.WriteLine($"Task1 運行中... {i + 1}/10");
        await Task.Delay(200, ct);
    }
    Console.WriteLine("Task1 完成");
}

async Task Task2(CancellationToken ct)
{
    for (int i = 0; i < 15; i++)
    {
        ct.ThrowIfCancellationRequested();
        Console.WriteLine($"Task2 運行中... {i + 1}/15");
        await Task.Delay(150, ct);
    }
    Console.WriteLine("Task2 完成");
}

async Task Task3(CancellationToken ct)
{
    for (int i = 0; i < 20; i++)
    {
        ct.ThrowIfCancellationRequested();
        Console.WriteLine($"Task3 運行中... {i + 1}/20");
        await Task.Delay(100, ct);
    }
    Console.WriteLine("Task3 完成");
}

使用示例:

var cts = new CancellationTokenSource();

// 啟動復(fù)雜操作
var operationTask = ComplexOperationAsync(cts.Token);

// 等待3秒后取消
await Task.Delay(3000);
Console.WriteLine("取消所有任務(wù)...");
cts.Cancel();

try
{
    await operationTask;
}
catch (OperationCanceledException)
{
    Console.WriteLine("操作已取消");
}

輸出示例:

Task1 運行中... 1/10
Task2 運行中... 1/15
Task3 運行中... 1/20
Task1 運行中... 2/10
Task2 運行中... 2/15
Task3 運行中... 2/20
Task1 運行中... 3/10
Task2 運行中... 3/15
Task3 運行中... 3/20
取消所有任務(wù)...
某個或所有任務(wù)被取消
操作已取消

4.3 超時控制

超時控制是 CancellationToken 的一個重要應(yīng)用場景。在運動控制系統(tǒng)中,我們經(jīng)常需要設(shè)置超時,防止某個操作長時間卡死。

方式1:使用 CancellationTokenSource 的構(gòu)造函數(shù)

// 方式1:使用帶超時的構(gòu)造函數(shù)
// 創(chuàng)建一個會在指定時間后自動取消的 CancellationTokenSource
async Task<T> WithTimeoutAsync<T>(
    Task<T> task,
    TimeSpan timeout)
{
    // 創(chuàng)建一個會在 timeout 時間后自動取消的 CancellationTokenSource
    using var cts = new CancellationTokenSource(timeout);

    try
    {
        // 等待任務(wù)完成
        return await task;
    }
    catch (OperationCanceledException) when (!cts.Token.IsCancellationRequested)
    {
        // 任務(wù)自己取消了,不是超時
        // 這種情況下,異常是由任務(wù)本身拋出的,而不是超時
        throw;
    }
    catch (OperationCanceledException)
    {
        // cts.Token.IsCancellationRequested 為 true
        // 說明是超時導(dǎo)致的取消
        throw new TimeoutException($"操作超時({timeout.TotalSeconds}秒)");
    }
}

方式2:使用 Task.WhenAny 實現(xiàn)超時

// 方式2:使用 Task.WhenAny 比較原任務(wù)和超時任務(wù)
async Task<T> WithTimeoutAsync<T>(
    Task<T> task,
    TimeSpan timeout)
{
    // 創(chuàng)建超時任務(wù)
    var timeoutTask = Task.Delay(timeout);

    // 等待任一任務(wù)完成
    var completedTask = await Task.WhenAny(task, timeoutTask);

    // 如果超時任務(wù)先完成,拋出超時異常
    if (completedTask == timeoutTask)
    {
        throw new TimeoutException($"操作超時({timeout.TotalSeconds}秒)");
    }

    // 否則返回原任務(wù)的結(jié)果
    return await task;
}

方式3:結(jié)合 CancellationToken 和 Task.WhenAny

// 方式3:結(jié)合使用 CancellationToken 和 Task.WhenAny
// 這樣可以真正取消原任務(wù),而不只是超時
async Task<T> WithTimeoutAsync<T>(
    Task<T> task,
    TimeSpan timeout)
{
    // 創(chuàng)建超時的 CancellationTokenSource
    using var cts = new CancellationTokenSource(timeout);

    try
    {
        // 使用 Task.WhenAny 等待任一任務(wù)完成
        var completedTask = await Task.WhenAny(task, Task.Delay(timeout, cts.Token));

        // 如果原任務(wù)先完成,返回結(jié)果
        if (completedTask == task)
        {
            return await task;
        }

        // 如果超時,拋出異常
        throw new TimeoutException($"操作超時({timeout.TotalSeconds}秒)");
    }
    catch (OperationCanceledException) when (cts.Token.IsCancellationRequested)
    {
        // 確認是超時導(dǎo)致的取消
        throw new TimeoutException($"操作超時({timeout.TotalSeconds}秒)");
    }
}

以上就是C#使用Cancellation取消異步任務(wù)的全過程的詳細內(nèi)容,更多關(guān)于C# Cancellation取消異步任務(wù)的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 基于C#編寫一個HTTP服務(wù)器和客戶端

    基于C#編寫一個HTTP服務(wù)器和客戶端

    本文介紹了如何在C# WinForm程序中實現(xiàn)一個簡易的HTTP服務(wù)器,能夠處理JSON格式的GET和POST請求,通過使用.NET自帶的HttpListener類,可以實現(xiàn)多端口監(jiān)聽和異步請求處理,需要的朋友可以參考下
    2026-01-01
  • 詳解C#如何使用屏障實現(xiàn)多線程并發(fā)操作保持同步

    詳解C#如何使用屏障實現(xiàn)多線程并發(fā)操作保持同步

    這篇文章主要為大家詳細介紹了C#如何使用屏障實現(xiàn)多線程并發(fā)操作保持同步,文中的示例代碼簡潔易懂,具有一定的借鑒價值,有需要的小伙伴可以參考下
    2024-01-01
  • C#調(diào)用OutLokk實現(xiàn)發(fā)送郵件

    C#調(diào)用OutLokk實現(xiàn)發(fā)送郵件

    這篇文章主要為大家詳細介紹了如何利用C#調(diào)用OutLokk實現(xiàn)發(fā)送郵件的功能,文中的示例代碼講解詳細,對我們學(xué)習(xí)C#有一定的幫助,感興趣的小伙伴可以跟隨小編一起了解一下
    2022-12-12
  • C#中利用代理實現(xiàn)觀察者設(shè)計模式詳解

    C#中利用代理實現(xiàn)觀察者設(shè)計模式詳解

    學(xué)習(xí)模式注重精髓而非模板,本文為了便于說明假定了三方并對三方功能進行了劃分,實際應(yīng)用并不拘泥于此。如果情況合適將數(shù)據(jù)(文檔)類設(shè)計為單件模式也是一種很不錯的選擇
    2014-01-01
  • VsCode使用EmmyLua插件調(diào)試Unity工程Lua代碼的詳細步驟

    VsCode使用EmmyLua插件調(diào)試Unity工程Lua代碼的詳細步驟

    這篇文章主要介紹了VsCode使用EmmyLua插件調(diào)試Unity工程Lua代碼,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-08-08
  • C#實現(xiàn)將Email地址轉(zhuǎn)成圖片顯示的方法

    C#實現(xiàn)將Email地址轉(zhuǎn)成圖片顯示的方法

    這篇文章主要介紹了C#實現(xiàn)將Email地址轉(zhuǎn)成圖片顯示的方法,涉及C#操作圖片的相關(guān)技巧,需要的朋友可以參考下
    2015-06-06
  • C#正則過濾HTML標簽并保留指定標簽的方法

    C#正則過濾HTML標簽并保留指定標簽的方法

    這篇文章主要介紹了C#正則過濾HTML標簽并保留指定標簽的方法,涉及C#針對頁面HTML元素正則匹配與替換相關(guān)操作技巧,需要的朋友可以參考下
    2017-06-06
  • 自動輸出類的字段值實用代碼分享

    自動輸出類的字段值實用代碼分享

    有點時候在測試的時候希望打印輸出返回對象的各字段的值,采用下面的代碼可以很方便的列出對象的各字段值
    2013-12-12
  • C#實現(xiàn)五子棋游戲

    C#實現(xiàn)五子棋游戲

    這篇文章主要為大家詳細介紹了C#實現(xiàn)五子棋游戲,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-01-01
  • C#中文件名或文件路徑非法字符判斷方法

    C#中文件名或文件路徑非法字符判斷方法

    這篇文章主要介紹了C#中文件名或文件路徑非法字符判斷方法,本文主要使用了內(nèi)置的GetInvalidFileNameChars方法實現(xiàn)非法字符判斷,需要的朋友可以參考下
    2015-06-06

最新評論

镇康县| 社旗县| 固原市| 洪雅县| 西畴县| 婺源县| 壶关县| 长治县| 安吉县| 尼勒克县| 克东县| 滕州市| 鹤岗市| 石阡县| 姚安县| 农安县| 阜宁县| 左权县| 阿克苏市| 本溪| 仪陇县| 涿州市| 波密县| 崇信县| 乐陵市| 玉门市| 石嘴山市| 开鲁县| 库车县| 黑河市| 海阳市| 武穴市| 辛集市| 朝阳区| 象山县| 义乌市| 沛县| 额尔古纳市| 甘泉县| 故城县| 蒙阴县|