在使用 public async Task StartRealtimePollingAsync(TimeSpan interval, CancellationToken cancellationToken) 方法时,CancellationToken 是一个用于支持任务取消的机制,通常由调用方创建并传递。以下是关于如何正确创建、传递和使用 cancellationToken 的详细说明,结合你的实时轮询场景,确保代码清晰、可靠且易于维护。
什么是 CancellationToken?
-
CancellationToken 是 .NET 提供的一种机制,用于在异步操作(如轮询硬件数据)中通知任务取消。
-
它通常与 CancellationTokenSource 一起使用,后者负责创建和控制取消信号。
-
在你的场景中,StartRealtimePollingAsync 使用 CancellationToken 允许外部调用方在需要时(例如系统关闭、用户操作或错误状态)取消轮询任务。
如何传递 CancellationToken?
以下是创建和传递 CancellationToken 的步骤,以及在你的代码中的具体实现方式。
1. 创建 CancellationTokenSource
-
调用方需要创建一个 CancellationTokenSource 实例,用于生成 CancellationToken 并控制取消。
-
示例:
csharp
var cts = new CancellationTokenSource(); CancellationToken token = cts.Token;
-
可选设置超时:
-
如果希望轮询在特定时间后自动停止,可以设置超时。
-
示例:
csharp
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); // 30秒后自动取消
-
2. 传递 CancellationToken
-
将 cts.Token 作为参数传递给 StartRealtimePollingAsync。
-
示例:
csharp
public async Task StartPollingExampleAsync() { using var cts = new CancellationTokenSource(); var interval = TimeSpan.FromMilliseconds(100); // 每100ms轮询一次 try { await testChannel.StartRealtimePollingAsync(interval, cts.Token); } catch (OperationCanceledException) { Logger.Info("轮询任务已取消"); } }
-
注意:
-
使用 using 确保 CancellationTokenSource 在不再需要时被正确释放。
-
OperationCanceledException 是取消时抛出的异常,需捕获以优雅处理。
-
3. 触发取消
-
调用方可以通过调用 cts.Cancel() 触发取消,通知 StartRealtimePollingAsync 停止轮询。
-
示例:
csharp
public async Task StartAndCancelPollingAsync() { using var cts = new CancellationTokenSource(); var interval = TimeSpan.FromMilliseconds(100); // 启动轮询 var pollingTask = testChannel.StartRealtimePollingAsync(interval, cts.Token); // 模拟5秒后取消 await Task.Delay(5000); cts.Cancel(); try { await pollingTask; } catch (OperationCanceledException) { Logger.Info("轮询任务已取消"); } }
-
场景:
-
系统关闭时(如调用 SystemMgr.CloseSystemAsync)。
-
用户手动停止测试(如通过 UI 按钮)。
-
硬件错误触发停止轮询。
-
4. 在 StartRealtimePollingAsync 中使用 CancellationToken
-
方法内部需要检查 cancellationToken.IsCancellationRequested 或将 CancellationToken 传递给依赖的异步操作(如 Task.Delay、QueryDataAsync)。
-
你的原始实现:
csharp
public async Task StartRealtimePollingAsync(TimeSpan interval, CancellationToken cancellationToken) { using var timer = new PeriodicTimer(interval); while (await timer.WaitForNextTickAsync(cancellationToken)) { try { var (daqResult, adapterBoardResult) = await QueryHardwareDataAsync(SectionId, cancellationToken); _dataCache[SectionId] = (daqResult, adapterBoardResult); await ProcessBusinessLogicAsync(daqResult, adapterBoardResult, cancellationToken); } catch (IOException ex) { Logger.Error($"硬件查询失败: SectionId={SectionId}", ex); await NotifyErrorAsync("硬件通信错误", ex, cancellationToken); } catch (Exception ex) { Logger.Error($"未知错误: SectionId={SectionId}", ex); await NotifyErrorAsync("未知硬件错误", ex, cancellationToken); } } }
-
优化点:
-
确保所有异步调用(如 QueryHardwareDataAsync、ProcessBusinessLogicAsync)都传递 cancellationToken。
-
在捕获异常后,检查是否因取消抛出异常,以避免重复记录。
-
示例优化:
csharp
public async Task StartRealtimePollingAsync(TimeSpan interval, CancellationToken cancellationToken) { using var timer = new PeriodicTimer(interval); while (await timer.WaitForNextTickAsync(cancellationToken)) { try { var (daqResult, adapterBoardResult) = await QueryHardwareDataAsync(SectionId, cancellationToken); if (daqResult != null && adapterBoardResult != null) { _dataCache[SectionId] = (daqResult, adapterBoardResult); await ProcessBusinessLogicAsync(daqResult, adapterBoardResult, cancellationToken); } else { Logger.Warn($"无效数据: SectionId={SectionId}"); } } catch (OperationCanceledException) { Logger.Info($"轮询任务取消: SectionId={SectionId}"); throw; // 重新抛出,确保调用方知道取消 } catch (IOException ex) { Logger.Error($"硬件查询失败: SectionId={SectionId}", ex); await NotifyErrorAsync("硬件通信错误", ex, cancellationToken); } catch (Exception ex) { Logger.Error($"未知错误: SectionId={SectionId}", ex); await NotifyErrorAsync("未知硬件错误", ex, cancellationToken); } } }
-
5. 在依赖方法中传递 CancellationToken
-
确保 QueryHardwareDataAsync 和 ProcessBusinessLogicAsync 等方法正确处理 CancellationToken。
-
示例:
csharp
private async Task<(HardwareResult DAQResult, HardwareResult AdapterBoardResult)> QueryHardwareDataAsync(string sectionId, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); // 提前检查取消 Logger.Debug($"查询数据: SectionId={sectionId}"); var daqResult = await QueryDataWithLockAsync(HardwareCommands.DAQRead, sectionId, cancellationToken); var adapterBoardResult = (DateTime.UtcNow - _lastAdapterBoardQueryTime) < _cacheExpiration && _cachedAdapterBoardResult != null ? _cachedAdapterBoardResult : await QueryDataWithLockAsync(HardwareCommands.DAQReadAdapterBoard, sectionId, cancellationToken); if (adapterBoardResult != null) { _cachedAdapterBoardResult = adapterBoardResult; _lastAdapterBoardQueryTime = DateTime.UtcNow; } return (daqResult, adapterBoardResult); } private async Task<HardwareResult> QueryDataWithLockAsync(string command, string sectionId, CancellationToken cancellationToken) { if (await HardwareMgr.DAQLocks[sectionId].WaitAsync(TimeSpan.FromSeconds(5), cancellationToken)) { try { return await _hardware.QueryDataAsync(command, cancellationToken) as HardwareResult; } finally { HardwareMgr.DAQLocks[sectionId].Release(); } } Logger.Warn($"获取锁超时: SectionId={sectionId}, 命令={command}"); return null; }
-
注意:
-
QueryDataAsync 需支持 CancellationToken,确保硬件通信可取消。
-
如果硬件不支持取消,需在超时后记录警告并返回默认值。
-
6. 系统级传递
-
在系统级(如 SystemMgr)中,通常需要管理多个 TestChannel 的轮询任务,并统一取消。
-
示例:
csharp
public class SystemMgr { private readonly List<TestChannel> _channels; private CancellationTokenSource _systemCts; public async Task StartSystemAsync() { _systemCts = new CancellationTokenSource(); var tasks = _channels.Select(channel => channel.StartRealtimePollingAsync(TimeSpan.FromMilliseconds(100), _systemCts.Token)); await Task.WhenAll(tasks); } public async Task StopSystemAsync() { if (_systemCts != null) { _systemCts.Cancel(); try { await Task.Delay(1000); // 等待任务优雅退出 } catch (OperationCanceledException) { Logger.Info("系统轮询任务已取消"); } _systemCts.Dispose(); _systemCts = null; } } }
-
场景:
-
系统初始化时启动所有通道的轮询。
-
系统关闭(如 CloseSystemAsync)时统一取消。
-
7. 用户交互
-
如果轮询由用户操作触发(如 UI 按钮),可以通过 UI 事件创建和取消 CancellationTokenSource。
-
示例(WPF/WinForms):
csharp
private CancellationTokenSource _cts; private async void StartPollingButton_Click(object sender, EventArgs e) { _cts = new CancellationTokenSource(); try { await testChannel.StartRealtimePollingAsync(TimeSpan.FromMilliseconds(100), _cts.Token); } catch (OperationCanceledException) { MessageBox.Show("轮询已取消"); } } private void StopPollingButton_Click(object sender, EventArgs e) { _cts?.Cancel(); _cts?.Dispose(); _cts = null; }
最佳实践
-
始终使用 using 或 Dispose:
-
确保 CancellationTokenSource 在不再需要时释放资源。
-
示例:using var cts = new CancellationTokenSource();
-
-
捕获 OperationCanceledException:
-
在调用方和方法内部捕获取消异常,避免未处理异常。
-
示例:catch (OperationCanceledException) { Logger.Info("任务取消"); }
-
-
传递链路完整:
-
确保 CancellationToken 从调用方传递到所有依赖的异步方法(如 QueryDataAsync、Task.Delay)。
-
示例:await Task.Delay(100, cancellationToken);
-
-
超时机制:
-
为长时间运行的任务设置超时,防止无限等待。
-
示例:var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
-
-
日志记录:
-
记录取消事件和异常,方便调试。
-
示例:Logger.Info($"轮询任务取消: SectionId={SectionId}");
-
-
测试取消逻辑:
-
编写单元测试验证取消行为。
-
示例:
csharp
[Fact] public async Task StartRealtimePolling_Cancel_StopsPolling() { var cts = new CancellationTokenSource(); var channel = new TestChannel(new Mock<IHardwareAdaptor>().Object, new Mock<IConfigProvider>().Object); var task = channel.StartRealtimePollingAsync(TimeSpan.FromMilliseconds(100), cts.Token); cts.Cancel(); await Assert.ThrowsAsync<OperationCanceledException>(() => task); }
-
传递 CancellationToken 的完整示例
以下是结合系统管理和用户交互的完整代码,展示如何传递 CancellationToken:
csharp
using log4net;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
public class TestChannel
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(TestChannel).Name);
private readonly IHardwareAdaptor _hardware;
private readonly IConfigProvider _configProvider;
private readonly ConcurrentDictionary<string, (HardwareResult DAQResult, HardwareResult AdapterBoardResult)> _dataCache = new();
private DateTime _lastAdapterBoardQueryTime;
private HardwareResult _cachedAdapterBoardResult;
private readonly TimeSpan _cacheExpiration = TimeSpan.FromSeconds(10);
private static class HardwareCommands
{
public const string DAQRead = "DAQREAD ";
public const string DAQReadAdapterBoard = "DAQREADADAPTERBOARD ";
}
public TestChannel(IHardwareAdaptor hardware, IConfigProvider configProvider)
{
_hardware = hardware;
_configProvider = configProvider;
}
public async Task StartRealtimePollingAsync(TimeSpan interval, CancellationToken cancellationToken)
{
Logger.Info($"启动实时轮询: SectionId={SectionId}, 间隔={interval.TotalMilliseconds}ms");
using var timer = new PeriodicTimer(interval);
while (await timer.WaitForNextTickAsync(cancellationToken))
{
try
{
var (daqResult, adapterBoardResult) = await QueryHardwareDataAsync(SectionId, cancellationToken);
if (daqResult != null && adapterBoardResult != null)
{
_dataCache[SectionId] = (daqResult, adapterBoardResult);
await ProcessBusinessLogicAsync(daqResult, adapterBoardResult, cancellationToken);
}
else
{
Logger.Warn($"无效数据: SectionId={SectionId}");
}
}
catch (OperationCanceledException)
{
Logger.Info($"轮询任务取消: SectionId={SectionId}");
throw;
}
catch (IOException ex)
{
Logger.Error($"硬件查询失败: SectionId={SectionId}", ex);
await NotifyErrorAsync("硬件通信错误", ex, cancellationToken);
}
catch (Exception ex)
{
Logger.Error($"未知错误: SectionId={SectionId}", ex);
await NotifyErrorAsync("未知硬件错误", ex, cancellationToken);
}
}
}
private async Task<(HardwareResult DAQResult, HardwareResult AdapterBoardResult)> QueryHardwareDataAsync(string sectionId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Logger.Debug($"查询数据: SectionId={sectionId}");
var daqResult = await QueryDataWithLockAsync(HardwareCommands.DAQRead, sectionId, cancellationToken);
var adapterBoardResult = (DateTime.UtcNow - _lastAdapterBoardQueryTime) < _cacheExpiration && _cachedAdapterBoardResult != null
? _cachedAdapterBoardResult
: await QueryDataWithLockAsync(HardwareCommands.DAQReadAdapterBoard, sectionId, cancellationToken);
if (adapterBoardResult != null)
{
_cachedAdapterBoardResult = adapterBoardResult;
_lastAdapterBoardQueryTime = DateTime.UtcNow;
}
Logger.Debug($"查询结果: DAQ={daqResult?.ResultMap["Voltage"]}, AdapterBoardTemp={adapterBoardResult?.ResultMap["AdapterBoardTemp"]}");
return (daqResult, adapterBoardResult);
}
private async Task<HardwareResult> QueryDataWithLockAsync(string command, string sectionId, CancellationToken cancellationToken)
{
if (await HardwareMgr.DAQLocks[sectionId].WaitAsync(TimeSpan.FromSeconds(5), cancellationToken))
{
try
{
return await _hardware.QueryDataAsync(command, cancellationToken) as HardwareResult;
}
finally
{
HardwareMgr.DAQLocks[sectionId].Release();
}
}
Logger.Warn($"获取锁超时: SectionId={sectionId}, 命令={command}");
return null;
}
private async Task ProcessBusinessLogicAsync(HardwareResult daqResult, HardwareResult adapterBoardResult, CancellationToken cancellationToken)
{
// 业务判定逻辑(参考之前的优化)
await Task.CompletedTask;
}
private async Task NotifyErrorAsync(string message, Exception ex, CancellationToken cancellationToken)
{
SystemNotifEvent.HardErrorNotifEvent(this, message, ex.Message);
await Task.CompletedTask;
}
public string SectionId { get; set; }
}
public class SystemMgr
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(SystemMgr).Name);
private readonly List<TestChannel> _channels = new();
private CancellationTokenSource _systemCts;
public async Task StartSystemAsync()
{
_systemCts = new CancellationTokenSource();
var interval = TimeSpan.FromMilliseconds(100);
var tasks = _channels.Select(channel =>
channel.StartRealtimePollingAsync(interval, _systemCts.Token));
try
{
await Task.WhenAll(tasks);
}
catch (OperationCanceledException)
{
Logger.Info("所有轮询任务已取消");
}
}
public async Task StopSystemAsync()
{
if (_systemCts != null)
{
_systemCts.Cancel();
try
{
await Task.Delay(1000); // 等待任务退出
}
catch (OperationCanceledException)
{
Logger.Info("系统停止完成");
}
_systemCts.Dispose();
_systemCts = null;
}
}
}
总结
-
创建:使用 CancellationTokenSource 创建 CancellationToken,通过 cts.Token 传递。
-
传递:将 CancellationToken 作为参数传递给 StartRealtimePollingAsync 及依赖方法。
-
取消:通过 cts.Cancel() 触发取消,捕获 OperationCanceledException 处理退出逻辑。
-
系统级:在 SystemMgr 中统一管理多个通道的 CancellationTokenSource,支持系统级取消。
-
用户交互:通过 UI 事件(如按钮点击)创建和取消 CancellationTokenSource。
如果需要进一步定制(如特定取消场景、硬件协议支持取消的实现),请提供更多细节,我可以提供更具体的方案!