【C#】CancellationTokenSource 为任务或线程提供一种优雅的方式来支持取消操作
CancellationTokenSource
是 .NET 中用于管理任务或异步操作的取消机制的一个核心类。
核心功能
-
生成取消令牌 (
CancellationToken
)CancellationTokenSource
是一个令牌的生产者,用来创建和管理CancellationToken
。CancellationToken
是取消机制的消费者,可以被传递到任务或异步操作中,通知它们“该停止了”。
-
触发取消
- 调用
CancellationTokenSource.Cancel()
可以触发所有使用该令牌的任务或线程的取消操作。
- 调用
-
取消协作
- 它是 协作取消 的基础:任务或线程本身检查令牌状态并决定何时停止,而不是被强制终止。
使用场景
CancellationTokenSource
和 CancellationToken
常见于以下场景:
- 异步任务 (
async/await
) 的取消。 - 多线程任务的优雅停止。
- 可控的长时间运行操作,比如轮询或计算。
类的主要成员
-
属性
Token
: 获取与当前CancellationTokenSource
关联的CancellationToken
。CancellationToken token = cancellationTokenSource.Token;
IsCancellationRequested
: 检查取消是否已经被请求。if (cancellationTokenSource.Token.IsCancellationRequested) { // Handle cancellation }
-
方法
Cancel()
: 触发取消令牌。cancellationTokenSource.Cancel();
Dispose()
: 释放CancellationTokenSource
所使用的资源。cancellationTokenSource.Dispose();
简单工作原理
-
创建一个取消源:
CancellationTokenSource cts = new CancellationTokenSource();
-
获取令牌并传递给任务:
CancellationToken token = cts.Token;
-
任务中检查令牌:
- 周期性地检查令牌的
IsCancellationRequested
属性。 - 或直接调用
token.ThrowIfCancellationRequested()
抛出OperationCanceledException
。
- 周期性地检查令牌的
-
请求取消:
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
}
}
}
示例中的流程
- 主线程创建
CancellationTokenSource
。 - 主线程启动任务,并将
CancellationToken
传递给任务。 - 任务定期检查
token.IsCancellationRequested
或使用ThrowIfCancellationRequested()
。 - 用户按下键触发
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 线程)可以直接操作该控件。如果尝试从其他线程访问或更新控件,程序可能会抛出异常。
核心概念
-
InvokeRequired
属性- 检查当前线程是否是控件所属的 UI 线程。
- 如果访问控件的线程不是创建它的线程,则返回
true
。 - 常见于多线程环境中,比如任务 (
Task
)、线程 (Thread
) 或异步操作中。
-
Invoke
方法- 将操作委托给控件的创建线程执行。
- 通过
Invoke
调用操作,确保代码在控件的 UI 线程上运行,避免线程安全问题。
-
Action
委托- 这里使用了匿名方法(
()=>{}
)作为Action
委托。 Action
是一种无返回值的委托,用于包装控件更新的代码。 【值得展开讲】
- 这里使用了匿名方法(
-
为什么 ?
- 如果不通过
Invoke
方法直接在非 UI 线程上更新控件,程序会抛出InvalidOperationException
,因为控件只能由其创建线程访问。
- 如果不通过
1. 检查是否需要 Invoke
if (edtUpdateMonitorResult.InvokeRequired)
- 检查调用该代码的线程是否与创建
edtUpdateMonitorResult
的线程不同。 - 如果不同,返回
true
,表示需要通过Invoke
进行线程安全的调用。
2. 使用 Invoke
更新控件
edtUpdateMonitorResult.Invoke(new Action(() => edtGetAllParamResult.Text = result));
- 如果
InvokeRequired
为true
,则调用Invoke
。 - 通过
Action
委托,定义要执行的代码:() => edtGetAllParamResult.Text = result;
- 这是一个 lambda 表达式,表示设置控件
edtGetAllParamResult.Text
的值为result
。
- 这是一个 lambda 表达式,表示设置控件
- 这段代码会被“转移”到创建控件的线程上安全执行。
3. 如果 InvokeRequired
为 false
- 如果当前线程已经是 UI 线程,则无需使用
Invoke
,直接执行控件的更新操作即可。
为什么只在 UI 控件中需要?
-
Windows Forms 的线程模型:
- 控件的底层依赖于 Win32 消息循环(Message Loop)。
- 每个控件都有一个与之关联的线程,通常是主线程。
- 为了避免线程冲突,只有创建控件的线程可以直接操作它。
-
典型问题场景:
- 使用后台线程(如
Thread
或Task
)执行耗时操作,并尝试直接更新 UI 控件时,会导致异常。
- 使用后台线程(如
适用场景
-
从后台线程更新 UI:
- 在异步任务中,获取数据并更新控件。
- 如监控线程中周期性读取串口数据并更新文本框。
-
线程安全的控件访问:
- 确保跨线程操作控件时不会导致冲突。
小结
这段代码的主要目的是保证线程安全的控件更新:
InvokeRequired
检查是否需要切换到 UI 线程。Invoke
把任务委托到控件的 UI 线程执行。