当前位置: 首页 > article >正文

【C#】CancellationTokenSource 为任务或线程提供一种优雅的方式来支持取消操作

CancellationTokenSource 是 .NET 中用于管理任务或异步操作的取消机制的一个核心类。


核心功能

  1. 生成取消令牌 (CancellationToken)

    • CancellationTokenSource 是一个令牌的生产者,用来创建和管理 CancellationToken
    • CancellationToken 是取消机制的消费者,可以被传递到任务或异步操作中,通知它们“该停止了”。
  2. 触发取消

    • 调用 CancellationTokenSource.Cancel() 可以触发所有使用该令牌的任务或线程的取消操作。
  3. 取消协作

    • 它是 协作取消 的基础:任务或线程本身检查令牌状态并决定何时停止,而不是被强制终止。

使用场景

CancellationTokenSourceCancellationToken 常见于以下场景:

  • 异步任务 (async/await) 的取消。
  • 多线程任务的优雅停止。
  • 可控的长时间运行操作,比如轮询或计算。

类的主要成员

  1. 属性

    • Token: 获取与当前 CancellationTokenSource 关联的 CancellationToken
      CancellationToken token = cancellationTokenSource.Token;
      
    • IsCancellationRequested: 检查取消是否已经被请求。
      if (cancellationTokenSource.Token.IsCancellationRequested) {
          // Handle cancellation
      }
      
  2. 方法

    • Cancel(): 触发取消令牌。
      cancellationTokenSource.Cancel();
      
    • Dispose(): 释放 CancellationTokenSource 所使用的资源。
      cancellationTokenSource.Dispose();
      

简单工作原理

  1. 创建一个取消源:

    CancellationTokenSource cts = new CancellationTokenSource();
    
  2. 获取令牌并传递给任务:

    CancellationToken token = cts.Token;
    
  3. 任务中检查令牌:

    • 周期性地检查令牌的 IsCancellationRequested 属性。
    • 或直接调用 token.ThrowIfCancellationRequested() 抛出 OperationCanceledException
  4. 请求取消:

    cts.Cancel();
    

** 示例1 **

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();

        Task task = Task.Run(() => DoWork(cts.Token), cts.Token);

        Console.WriteLine("Press any key to cancel...");
        Console.ReadKey();
        cts.Cancel();

        try
        {
            await task;
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Task was cancelled.");
        }
        finally
        {
            cts.Dispose();
        }
    }

    static void DoWork(CancellationToken token)
    {
        for (int i = 0; i < 10; i++)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("Cancellation requested.");
                token.ThrowIfCancellationRequested(); // Gracefully exit.
            }
            Console.WriteLine($"Working {i}...");
            Thread.Sleep(1000); // Simulate work
        }
    }
}

示例中的流程

  1. 主线程创建 CancellationTokenSource
  2. 主线程启动任务,并将 CancellationToken 传递给任务。
  3. 任务定期检查 token.IsCancellationRequested 或使用 ThrowIfCancellationRequested()
  4. 用户按下键触发 cts.Cancel(),任务收到取消信号并安全退出。

** 示例2 **

private CancellationTokenSource CancellingReadingRecordTokenSource;
private bool _isReadingRecord = false;


  private async  void btnReadWaring_Click(object sender, EventArgs e)
  {
    
      if (_isReadingRecord)
      {
          MessageBox.Show("_ ReadingRecord is already running.");
          return;

      }
      _isReadingRecord = true;
      CancellingReadingRecordTokenSource = new CancellationTokenSource();
      try
      {
          await Task.Run(() => ReadingRecordLoop(CancellingReadingRecordTokenSource.Token));
      }
      catch (OperationCanceledException)
      {
          // Handle cancellation gracefully if needed
      }
      finally
      {
          _isReadingRecord = false;
      }
  }
  private void ReadingRecordLoop(CancellationToken cancellationToken)
  {
      while (!cancellationToken.IsCancellationRequested)
      {
          string tmpStrRead = Common.mServoApi.dosomething();
          UpdateAlarmRecordResult(tmpStrRead);
          Thread.Sleep(50);  
      }
  }
  private void UpdateAlarmRecordResult(string result)
  {
      if (edtUpdateResult.InvokeRequired)
      {
          edtUpdateResult.Invoke(new Action(() => edtResult.Text = result));
      }
      else
      {
          edtUpdateResult.Text = result;
      }
  }
    

  private void btnStopReadWaring_Click(object sender, EventArgs e)
  {
      if (_isReadingRecord)
      {
          CancellingReadingRecordTokenSource?.Cancel();
      }
  }

补充 edtUpdateResult.InvokeRequired

这段代码的作用是确保跨线程访问控件时的线程安全性。在 Windows Forms 应用程序中,只有创建控件的线程(通常是主 UI 线程)可以直接操作该控件。如果尝试从其他线程访问或更新控件,程序可能会抛出异常。

核心概念
  1. InvokeRequired 属性

    • 检查当前线程是否是控件所属的 UI 线程。
    • 如果访问控件的线程不是创建它的线程,则返回 true
    • 常见于多线程环境中,比如任务 (Task)、线程 (Thread) 或异步操作中。
  2. Invoke 方法

    • 将操作委托给控件的创建线程执行。
    • 通过 Invoke 调用操作,确保代码在控件的 UI 线程上运行,避免线程安全问题。
  3. Action 委托

    • 这里使用了匿名方法(()=>{})作为 Action 委托。
    • Action 是一种无返回值的委托,用于包装控件更新的代码。 【值得展开讲】
  4. 为什么 ?

    • 如果不通过 Invoke 方法直接在非 UI 线程上更新控件,程序会抛出 InvalidOperationException,因为控件只能由其创建线程访问。

1. 检查是否需要 Invoke
if (edtUpdateMonitorResult.InvokeRequired)
  • 检查调用该代码的线程是否与创建 edtUpdateMonitorResult 的线程不同。
  • 如果不同,返回 true,表示需要通过 Invoke 进行线程安全的调用。
2. 使用 Invoke 更新控件
edtUpdateMonitorResult.Invoke(new Action(() => edtGetAllParamResult.Text = result));
  • 如果 InvokeRequiredtrue,则调用 Invoke
  • 通过 Action 委托,定义要执行的代码:
    () => edtGetAllParamResult.Text = result;
    
    • 这是一个 lambda 表达式,表示设置控件 edtGetAllParamResult.Text 的值为 result
  • 这段代码会被“转移”到创建控件的线程上安全执行。
3. 如果 InvokeRequiredfalse
  • 如果当前线程已经是 UI 线程,则无需使用 Invoke,直接执行控件的更新操作即可。

为什么只在 UI 控件中需要?

  • Windows Forms 的线程模型

    • 控件的底层依赖于 Win32 消息循环(Message Loop)。
    • 每个控件都有一个与之关联的线程,通常是主线程。
    • 为了避免线程冲突,只有创建控件的线程可以直接操作它。
  • 典型问题场景

    • 使用后台线程(如 ThreadTask)执行耗时操作,并尝试直接更新 UI 控件时,会导致异常。

适用场景

  1. 从后台线程更新 UI

    • 在异步任务中,获取数据并更新控件。
    • 如监控线程中周期性读取串口数据并更新文本框。
  2. 线程安全的控件访问

    • 确保跨线程操作控件时不会导致冲突。

小结

这段代码的主要目的是保证线程安全的控件更新:

  • InvokeRequired 检查是否需要切换到 UI 线程。
  • Invoke 把任务委托到控件的 UI 线程执行。

http://www.kler.cn/a/410372.html

相关文章:

  • oracle会话追踪
  • redis数据类型详解
  • 滑动窗口篇——如行云流水般的高效解法与智能之道(2)
  • 游戏引擎学习第22天
  • 【MySQL】数据库核心技术与应用指南
  • HarmonyOS应用开发中的页面路由与数据传输
  • HTML飞舞的爱心
  • 使用八爪鱼爬虫抓取汽车网站数据,分析舆情数据
  • Cocos creator 3.8 一些事件的使用,加载预制体的两种方式 5
  • 深入理解 MyBatis 的缓存机制:一级缓存与二级缓存
  • Java工程管理数字化智慧工地云平台SaaS源码 (PC端、移动端、大屏端)
  • 2022年计算机网络408考研真题解析
  • Qt中2D绘制系统
  • QT简易项目 数据库可视化界面 数据库编程SQLITE QT5.12.3环境 C++实现
  • Leetcode 3362. Zero Array Transformation III
  • JUC:Java内存模型JMM
  • 【深度学习】利用Java DL4J构建金融欺诈检测系统
  • libphone desktop编译
  • C++趣味编程玩转物联网:用树莓派Pico实现一位数码管动态显示
  • 大数据面试题每日练习 -- 解释RDD的概念
  • OSPF路由状态数据库、type 类型、完整的LSA
  • 华为OD机试真题-最大矩阵和-2024年OD统一考试(E卷)
  • node.js、nginx、iis、tomcat针对部署方面的简述
  • springboot/ssm综合小区管理系统Java社区物业停车缴费系统web物业源码
  • Python设计模式详解之13 —— 模板方法模式
  • 低速接口项目之串口Uart开发(二)——FIFO实现串口数据的收发回环测试