在 C# 中,异步编程是处理长时间运行任务(如 I/O 操作、网络请求等)的常见方式,而取消机制是异步编程中一个核心的功能,用于在需要时中断或取消正在执行的异步处理

在 C# 中,异步编程是处理长时间运行任务(如 I/O 操作、网络请求等)的常见方式,而取消机制是异步编程中一个重要的功能,用于在需要时中断或取消正在执行的异步操作。

C# 提供了 CancellationToken 和 CancellationTokenSource 来实现异步取消机制。本文将详细讲解 C# 中的异步取消机制,包括其核心概念、使用方法、注意事项以及示例代码。


1. 核心概念

1.1 CancellationToken 和 CancellationTokenSource

  • CancellationTokenSource:这是取消操作的控制中心,负责创建和管理 CancellationToken。通过它可以触发取消信号。
    • 常用方法:
      • Cancel():触发取消信号。
      • CancelAfter(TimeSpan):在指定时间后自动触发取消。
      • Dispose():释放资源。
  • CancellationToken:这是一个轻量级结构体,表示取消状态。它由 CancellationTokenSource 创建,用于传递给异步方法,让方法能够检测取消请求。
    • 常用属性和方法:
      • IsCancellationRequested:检查是否请求了取消。
      • ThrowIfCancellationRequested():如果取消被请求,抛出 OperationCanceledException。
      • Register(Action):注册一个回调,当取消发生时执行。

1.2 取消的工作原理

  • 异步方法通过接收 CancellationToken 参数,在执行过程中定期检查 IsCancellationRequested 或调用 ThrowIfCancellationRequested() 来响应取消请求。
  • 如果取消被触发,通常会抛出 OperationCanceledException,调用方可以捕获此异常以处理取消逻辑。
  • 取消是协作式的,即异步方法需要显式检查 CancellationToken 的状态,C# 不会强制终止线程或任务。

2. 使用方法

2.1 基本使用示例以下是一个简单的异步方法,展示如何使用 CancellationToken 实现取消:csharp

using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
// 创建 CancellationTokenSource
using var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// 启动异步任务
Task task = DoWorkAsync(token);
// 模拟用户按下取消
Console.WriteLine("按任意键取消...");
Console.ReadKey();
cts.Cancel(); // 触发取消
try
{
await task;
Console.WriteLine("任务完成");
}
catch (OperationCanceledException)
{
Console.WriteLine("任务被取消");
}
}
static async Task DoWorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
// 检查取消请求
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"处理中 {i}...");
await Task.Delay(1000, cancellationToken); // 模拟异步工作
}
}
}

运行结果:

  • 如果用户在任务完成前按下任意键,cts.Cancel() 会触发取消,ThrowIfCancellationRequested() 抛出 OperationCanceledException,程序输出“任务被取消”。
  • 如果未取消,任务会循环 10 次并输出“任务完成”。

2.2 关键点解析

  1. 传递 CancellationToken:
    • 异步方法通常在参数中接收 CancellationToken,以便在内部检查取消状态。
    • 标准库中的许多异步方法(如 Task.Delay、HttpClient.GetAsync 等)都支持 CancellationToken。
  2. 抛出异常:
    • 使用 ThrowIfCancellationRequested() 是最简单的方式,它会在取消时抛出 OperationCanceledException,无需手动检查 IsCancellationRequested。
  3. 资源清理:
    • CancellationTokenSource 实现了 IDisposable,应在不再需要时调用 Dispose()(通常使用 using 语句)。
  4. 协作式取消:
    • 异步方法必须主动检查 CancellationToken,否则取消请求会被忽略。例如,在上面的代码中,如果不调用 ThrowIfCancellationRequested(),任务会继续运行。

3. 高级用法

3.1 超时取消可以使用 CancelAfter 或构造函数中的超时参数来实现自动取消:csharp

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // 5 秒后自动取消
// 或者
cts.CancelAfter(TimeSpan.FromSeconds(5));

示例:csharp

static async Task Main(string[] args)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
try
{
await DoWorkAsync(cts.Token);
Console.WriteLine("任务完成");
}
catch (OperationCanceledException)
{
Console.WriteLine("任务因超时被取消");
}
}
static async Task DoWorkAsync(CancellationToken cancellationToken)
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine("工作中...");
await Task.Delay(1000, cancellationToken);
}
}

运行结果:

  • 任务会在 3 秒后被取消,输出“任务因超时被取消”。

3.2 组合多个 CancellationToken可以使用 CancellationTokenSource.CreateLinkedTokenSource 组合多个 CancellationToken,当任意一个源取消时,组合的 token 也会取消:csharp

using var cts1 = new CancellationTokenSource();
using var cts2 = new CancellationTokenSource();
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
Task task = DoWorkAsync(linkedCts.Token);
cts2.Cancel(); // 任何一个源取消都会触发

3.3 注册回调可以通过 Register 方法在取消时执行特定操作:csharp

static async Task Main(string[] args)
{
using var cts = new CancellationTokenSource();
cts.Token.Register(() => Console.WriteLine("取消回调被触发!"));
Task task = DoWorkAsync(cts.Token);
await Task.Delay(2000);
cts.Cancel();
}

3.4 在复杂异步流程中使用在复杂的异步流程中,可以将 CancellationToken 传递到所有相关方法,确保整个调用链支持取消。例如,使用 HttpClient:csharp

static async Task DownloadAsync(string url, CancellationToken cancellationToken)
{
using var client = new HttpClient();
var response = await client.GetAsync(url, cancellationToken);
string content = await response.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine($"下载内容长度:{content.Length}");
}

4. 注意事项

  1. 异常处理:
    • 总是捕获 OperationCanceledException,以避免未处理的异常导致程序崩溃。
    • 如果任务被取消,Task.Status 会变为 Canceled。
  2. 避免忽略取消:
    • 如果异步方法不检查 CancellationToken,取消请求将无效。确保在循环或长时间运行的操作中定期检查。
  3. 线程安全:
    • CancellationTokenSource 是线程安全的,可以从多个线程调用 Cancel()。
    • CancellationToken 是只读的,传递给多个方法不会引发并发问题。
  4. 资源管理:
    • 不要重复使用已取消的 CancellationTokenSource,取消后它无法重置。
    • 总是确保 CancellationTokenSource 被正确释放(通过 Dispose 或 using)。
  5. 性能考虑:
    • 频繁创建和销毁 CancellationTokenSource 可能有轻微性能开销,在高频场景中可以重用 CancellationTokenSource。

5. 实际应用场景

  1. 用户取消操作:
    • 例如,GUI 应用程序中,用户点击“取消”按钮中断文件下载或数据处理。
  2. 超时处理:
    • 网络请求或数据库查询超过指定时间后自动取消。
  3. 任务协调:
    • 在微服务或并发任务中,某个任务失败时取消其他相关任务。
  4. 清理资源:
    • 在取消时执行清理操作,如关闭文件、网络连接等。

6. 常见问题与解答

Q1:如何判断任务是正常完成还是被取消?

  • 检查 Task.Status 是否为 Canceled,或者捕获 OperationCanceledException。

Q2:如果异步方法不检查 CancellationToken 会怎样?

  • 取消请求会被忽略,任务会继续运行直到完成。

Q3:如何在取消时释放资源?

  • 使用 CancellationToken.Register 注册清理回调,或在 catch (OperationCanceledException) 中手动清理。

Q4:可以重用 CancellationTokenSource 吗?

  • 不可以。一旦 CancellationTokenSource 被取消,它无法重置。需要创建新的实例。

7. 总结C# 的异步取消机制通过 CancellationToken 和 CancellationTokenSource 提供了一种强大而灵活的方式来管理异步任务的中止。核心是协作式取消,开发者需要显式检查取消状态并处理 OperationCanceledException。通过合理使用超时、回调和组合 token,可以实现复杂的取消逻辑,适用于各种实际场景。如果有更多具体问题或需要更深入的代码示例,请随时提问!

在 C# 中,异步编程是处理 I/O 操作、并发任务和高性能应用的常见方式,特别是在需要避免阻塞主线程的场景下。以下是关于 C# 异步编程(基于 async/await 和 Task)的最佳实践,结合异步取消机制(CancellationToken)等内容,详细讲解如何编写高效、可维护的异步代码。


1. 使用 async 和 await 正确

1.1 避免 async void

  • 问题:async void 方法无法被 await,异常处理困难,且容易导致未捕获的异常崩溃程序。
  • 最佳实践:始终返回 Task 或 Task<T>,除非是事件处理程序(如按钮点击事件)。csharp

    // 错误
    public async void DoWorkAsync() { ... }
    // 正确
    public async Task DoWorkAsync() { ... }

1.2 尽量使用 await 而不是 .Result 或 .Wait()

  • 问题:调用 Task.Result 或 Task.Wait() 会阻塞线程,可能导致死锁,尤其在 UI 线程或 ASP.NET 上下文中。
  • 最佳实践:始终使用 await 来等待异步操作完成。csharp

    // 错误
    var result = SomeAsyncMethod().Result;
    // 正确
    var result = await SomeAsyncMethod();

1.3 配置 ConfigureAwait(false)

  • 问题:默认情况下,await 会尝试恢复到调用时的同步上下文(如 UI 线程),这在不需要上下文的场景(如库代码)中会增加开销。
  • 最佳实践:在不需要同步上下文的代码中(如库函数或非 UI 代码),使用 ConfigureAwait(false)。csharp

    public async Task DoWorkAsync()
    {
    await Task.Delay(1000).ConfigureAwait(false);
    // 后续代码不会强制恢复到原始上下文
    }

2. 正确使用 CancellationToken2.1 始终支持取消

  • 最佳实践:在异步方法中添加 CancellationToken 参数,支持取消操作,确保方法是协作式的。csharp

    public async Task DoWorkAsync(CancellationToken cancellationToken)
    {
    cancellationToken.ThrowIfCancellationRequested();
    await Task.Delay(1000, cancellationToken);
    }

2.2 传递 CancellationToken 到所有异步调用

  • 最佳实践:确保 CancellationToken 被传递到所有支持它的异步方法(如 HttpClient、Task.Delay 等)。csharp

    public async Task DownloadAsync(string url, CancellationToken cancellationToken)
    {
    using var client = new HttpClient();
    var response = await client.GetAsync(url, cancellationToken).ConfigureAwait(false);
    }

2.3 处理取消异常

  • 最佳实践:捕获 OperationCanceledException,并根据需要处理取消逻辑。避免让取消异常传播到不合适的层级。csharp

    try
    {
    await DoWorkAsync(cancellationToken);
    }
    catch (OperationCanceledException)
    {
    Console.WriteLine("操作被取消");
    }

2.4 使用超时机制

  • 最佳实践:通过 CancellationTokenSource.CancelAfter 或构造函数设置超时,防止任务无限期运行。csharp

    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
    await DoWorkAsync(cts.Token);

3. 优化性能3.1 避免不必要的异步

  • 问题:将简单操作包装为异步方法会增加 Task 对象的开销。
  • 最佳实践:对于简单、无 I/O 操作的代码,直接返回结果或使用 ValueTask。csharp

    // 不必要
    public async Task GetValueAsync() => await Task.FromResult(42);
    // 优化
    public Task GetValueAsync() => Task.FromResult(42);
    // 或者使用 ValueTask
    public ValueTask GetValueAsync() => new ValueTask(42);

3.2 使用 ValueTask 减少分配

  • 最佳实践:在高性能场景(如频繁调用的方法)中,使用 ValueTask 替代 Task 来减少内存分配。csharp

    public async ValueTask ComputeAsync(int input)
    {
    if (input < 0) return 0; // 同步返回
    return await CalculateAsync(input); // 异步计算
    }

3.3 缓存 Task 结果

  • 最佳实践:对于重复使用的异步结果,缓存 Task 对象以避免重复计算。csharp

    private static readonly Task CachedResult = GetDataAsync();
    public Task GetCachedDataAsync() => CachedResult;

4. 错误处理4.1 捕获所有异常

  • 最佳实践:在异步方法中捕获所有可能的异常,并提供适当的错误处理逻辑。csharp

    public async Task ProcessAsync()
    {
    try
    {
    await DoWorkAsync();
    }
    catch (Exception ex)
    {
    Console.WriteLine($"错误: {ex.Message}");
    // 日志记录或重新抛出
    }
    }

4.2 处理聚合异常

  • 问题:多个并行任务失败时,Task.WhenAll 会抛出 AggregateException。
  • 最佳实践:使用 Task.WhenAll 时,展开异常以逐个处理。csharp

    var tasks = new[] { Task1(), Task2(), Task3() };
    try
    {
    await Task.WhenAll(tasks);
    }
    catch
    {
    foreach (var task in tasks)
    {
    if (task.Exception != null)
    Console.WriteLine(task.Exception.InnerException.Message);
    }
    }

5. 并行与并发5.1 区分异步和并行

  • 异步:用于 I/O 操作,释放线程等待结果。
  • 并行:用于 CPU 密集型任务,利用多核运行。
  • 最佳实践:I/O 操作使用 async/await,CPU 密集型任务使用 Parallel 或 Task.Run。csharp

    // I/O 操作
    public async Task DownloadAsync() => await new HttpClient().GetAsync("...");
    // CPU 密集型任务
    public Task ProcessDataAsync(int[] data) => Task.Run(() => Parallel.ForEach(data, Process));

5.2 控制并发度

  • 最佳实践:使用 SemaphoreSlim 或 ParallelOptions.MaxDegreeOfParallelism 限制并发任务数量,避免资源耗尽。csharp

    public async Task ProcessAllAsync(IEnumerable urls, CancellationToken cancellationToken)
    {
    using var semaphore = new SemaphoreSlim(3); // 限制最多 3 个并发
    var tasks = urls.Select(async url =>
    {
    await semaphore.WaitAsync(cancellationToken);
    try
    {
    await DownloadAsync(url, cancellationToken);
    }
    finally
    {
    semaphore.Release();
    }
    });
    await Task.WhenAll(tasks);
    }

6. 资源管理6.1 正确释放 CancellationTokenSource

  • 最佳实践:使用 using 或显式调用 Dispose 确保 CancellationTokenSource 被释放。csharp

    using var cts = new CancellationTokenSource();
    await DoWorkAsync(cts.Token);

6.2 避免资源泄漏

  • 最佳实践:在异步方法中使用 using 管理资源(如 HttpClient、Stream 等)。csharp

    public async Task ReadFileAsync(string path, CancellationToken cancellationToken)
    {
    using var stream = new FileStream(path, FileMode.Open);
    await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
    }

7. 测试与调试7.1 模拟异步场景

  • 最佳实践:使用 Task.Delay 或测试框架(如 xUnit、Moq)模拟异步操作和取消。csharp

    [Fact]
    public async Task TestCancellation()
    {
    using var cts = new CancellationTokenSource();
    cts.Cancel();
    await Assert.ThrowsAsync(() => DoWorkAsync(cts.Token));
    }

7.2 日志记录

  • 最佳实践:记录异步操作的开始、结束和异常,便于调试。csharp

    public async Task DoWorkAsync(CancellationToken cancellationToken)
    {
    Console.WriteLine("开始工作");
    try
    {
    await Task.Delay(1000, cancellationToken);
    Console.WriteLine("工作完成");
    }
    catch (Exception ex)
    {
    Console.WriteLine($"错误: {ex.Message}");
    throw;
    }
    }

8. ASP.NET 特定建议8.1 避免阻塞 ASP.NET 请求

  • 最佳实践:在 ASP.NET Core 中,始终使用 async 控制器方法,避免阻塞请求线程。csharp

    [HttpGet]
    public async Task GetDataAsync(CancellationToken cancellationToken)
    {
    var data = await _service.GetDataAsync(cancellationToken);
    return Ok(data);
    }

8.2 利用 ASP.NET 的 CancellationToken

  • 最佳实践:ASP.NET Core 控制器方法会自动注入 HttpContext.RequestAborted 作为 CancellationToken,用于处理客户端断开连接。csharp

    public async Task GetDataAsync(CancellationToken cancellationToken)
    {
    await _service.ProcessAsync(cancellationToken);
    return Ok();
    }

9. 总结C# 异步编程的最佳实践包括:

  • 使用 async Task 而非 async void,避免阻塞调用。
  • 正确使用 CancellationToken 支持取消和超时。
  • 优化性能,减少不必要的异步开销,使用 ValueTask 或缓存 Task。
  • 妥善处理异常和资源释放。
  • 在并行场景中控制并发度,区分 I/O 和 CPU 密集型任务。
  • 在 ASP.NET 中利用内置的取消机制和异步控制器。

在 C# 中,异步任务调度是异步编程的重要组成部分,用于管理异步任务的执行顺序、并发性以及资源分配。合理调度异步任务可以提高性能、避免资源竞争,并确保代码的可维护性。以下是对 C# 中异步任务调度的详细讲解,包括核心概念、实现方式、最佳实践和示例代码,结合之前提到的异步编程和取消机制的内容。


1. 异步任务调度的核心概念

1.1 什么是异步任务调度异步任务调度是指在异步编程中控制 Task 的执行方式,包括:

  • 执行时机:决定任务何时开始运行。
  • 并发控制:管理同时运行的任务数量。
  • 优先级:为任务分配优先级(尽管 C# 的默认调度器不直接支持优先级)。
  • 取消支持:结合 CancellationToken 实现任务中断。
  • 资源管理:避免过多任务导致 CPU 或 I/O 资源耗尽。

1.2 关键组件

  • Task 和 TaskScheduler:Task 是异步任务的核心,TaskScheduler 负责决定任务在哪个线程或上下文中执行。
  • 默认调度器:TaskScheduler.Default 使用线程池调度任务,适合大多数场景。
  • 同步上下文:在 UI 应用(如 WPF、WinForms)或 ASP.NET 中,TaskScheduler 可能与同步上下文(如 UI 线程)关联。
  • CancellationToken:用于取消任务,防止不必要的资源占用。
  • 并发控制工具:如 SemaphoreSlim、ConcurrentQueue 等,用于限制并发任务数量或实现任务队列。

1.3 典型场景

  • 批量处理:如同时下载多个文件,但限制并发数以避免服务器压力。
  • 后台任务:在服务中定期执行异步任务(如清理缓存)。
  • 依赖任务:按顺序执行一组有依赖关系的异步任务。
  • 高性能调度:在高负载场景下优化 CPU 和 I/O 使用。

2. 异步任务调度的实现方式2.1 使用默认 TaskSchedulerC# 的 Task 默认使用线程池调度,适合大多数异步操作。以下是一个简单的异步任务调度示例:csharp

using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Task[] tasks = new Task[3]
{
Task.Run(() => Console.WriteLine("任务 1")),
Task.Run(() => Console.WriteLine("任务 2")),
Task.Run(() => Console.WriteLine("任务 3"))
};
await Task.WhenAll(tasks); // 等待所有任务完成
Console.WriteLine("所有任务完成");
}
}

说明:

  • Task.Run 将任务调度到线程池。
  • Task.WhenAll 确保所有任务完成后再继续。

2.2 限制并发任务数量在处理大量异步任务时,限制并发数可以避免资源耗尽。使用 SemaphoreSlim 是一种常见方式:csharp

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var urls = new[] { "url1", "url2", "url3", "url4", "url5" };
using var semaphore = new SemaphoreSlim(2); // 限制最大并发为 2
var tasks = new List();
foreach (var url in urls)
{
tasks.Add(Task.Run(async () =>
{
await semaphore.WaitAsync();
try
{
await DownloadAsync(url);
}
finally
{
semaphore.Release();
}
}));
}
await Task.WhenAll(tasks);
Console.WriteLine("所有下载完成");
}
static async Task DownloadAsync(string url)
{
Console.WriteLine($"开始下载 {url}");
await Task.Delay(1000); // 模拟下载
Console.WriteLine($"完成下载 {url}");
}
}

说明:

  • SemaphoreSlim 限制最多 2 个任务同时运行。
  • 每个任务在完成时释放信号量,允许新任务进入。

2.3 结合 CancellationToken 实现取消在调度任务时,添加 CancellationToken 支持取消:csharp

using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
using var cts = new CancellationTokenSource();
var tasks = new[]
{
DoWorkAsync("任务 1", cts.Token),
DoWorkAsync("任务 2", cts.Token),
DoWorkAsync("任务 3", cts.Token)
};
Console.WriteLine("按任意键取消...");
Console.ReadKey();
cts.Cancel();
try
{
await Task.WhenAll(tasks);
}
catch (OperationCanceledException)
{
Console.WriteLine("任务被取消");
}
}
static async Task DoWorkAsync(string name, CancellationToken cancellationToken)
{
for (int i = 0; i < 5; i++)
{
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"{name} 运行中 {i}");
await Task.Delay(1000, cancellationToken);
}
}
}

说明:

  • 所有任务共享同一个 CancellationToken,取消时所有任务都会抛出 OperationCanceledException。

2.4 自定义任务队列对于需要按顺序或优先级调度的场景,可以实现一个简单的任务队列:csharp

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
class TaskQueue
{
private readonly ConcurrentQueue> _tasks = new();
private readonly SemaphoreSlim _semaphore;
private bool _isRunning;
public TaskQueue(int maxConcurrency)
{
_semaphore = new SemaphoreSlim(maxConcurrency);
}
public void Enqueue(Func task)
{
_tasks.Enqueue(task);
if (!_isRunning) StartProcessing();
}
private async void StartProcessing()
{
_isRunning = true;
while (_tasks.TryDequeue(out var task))
{
await _semaphore.WaitAsync();
_ = Task.Run(async () =>
{
try
{
await task(CancellationToken.None);
}
finally
{
_semaphore.Release();
}
});
}
_isRunning = false;
}
}
class Program
{
static async Task Main(string[] args)
{
var queue = new TaskQueue(2); // 最大并发 2
queue.Enqueue(async ct =>
{
Console.WriteLine("任务 1 开始");
await Task.Delay(1000, ct);
Console.WriteLine("任务 1 完成");
});
queue.Enqueue(async ct =>
{
Console.WriteLine("任务 2 开始");
await Task.Delay(1000, ct);
Console.WriteLine("任务 2 完成");
});
await Task.Delay(3000); // 等待任务完成
}
}

说明:

  • 使用 ConcurrentQueue 存储任务,SemaphoreSlim 控制并发。
  • 任务按加入顺序执行,但并发数受限。

2.5 使用 TPL Dataflow对于复杂的数据流或管道式任务调度,推荐使用 System.Threading.Tasks.Dataflow 库:csharp

using System;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
class Program
{
static async Task Main(string[] args)
{
var block = new ActionBlock(async url =>
{
Console.WriteLine($"开始下载 {url}");
await Task.Delay(1000); // 模拟下载
Console.WriteLine($"完成下载 {url}");
}, new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = 2 // 限制并发
});
block.Post("url1");
block.Post("url2");
block.Post("url3");
block.Complete();
await block.Completion;
Console.WriteLine("所有下载完成");
}
}

说明:

  • ActionBlock 是一个强大的工具,支持并发控制、错误处理和取消。
  • 适合数据流处理场景,如批量下载或处理管道。

3. 异步任务调度的最佳实践3.1 明确任务类型

  • I/O 密集型任务:直接使用 async/await,避免 Task.Run。
  • CPU 密集型任务:使用 Task.Run 将任务调度到线程池。csharp

    // I/O 任务
    public async Task DownloadAsync(string url, CancellationToken ct)
    => await new HttpClient().GetAsync(url, ct);
    // CPU 任务
    public Task ComputeAsync(int[] data)
    => Task.Run(() => data.Sum());

3.2 控制并发

  • 使用 SemaphoreSlim 或 ActionBlock 限制并发任务数量,防止资源耗尽。
  • 根据系统资源(如 CPU 核心数或网络带宽)调整并发度。

3.3 支持取消

  • 始终传递 CancellationToken 到任务,并定期检查取消状态。
  • 使用 CancellationTokenSource.CreateLinkedTokenSource 组合多个取消源。

3.4 错误处理

  • 在调度多个任务时,捕获 AggregateException 或单独处理每个任务的异常。csharp

    var tasks = new[] { Task1(), Task2() };
    try
    {
    await Task.WhenAll(tasks);
    }
    catch
    {
    foreach (var task in tasks)
    {
    if (task.Exception != null)
    Console.WriteLine(task.Exception.InnerException.Message);
    }
    }

3.5 避免死锁

  • 使用 ConfigureAwait(false) 避免同步上下文导致的死锁。
  • 不要在同步方法中调用 .Result 或 .Wait()。

3.6 监控和日志

  • 为每个任务添加日志,记录开始、结束和异常,便于调试。
  • 使用工具(如 Application Insights)监控任务性能。

3.7 选择合适的调度工具

  • 简单场景:使用 Task.WhenAll 或 Task.Run。
  • 并发控制:使用 SemaphoreSlim 或 TPL Dataflow。
  • 复杂管道:使用 TPL Dataflow 的 TransformBlock、BufferBlock 等。
  • 自定义调度:实现自定义 TaskScheduler(高级场景)。

4. 实际应用场景

  1. 批量下载:限制并发数,逐个下载文件并支持取消。
  2. 后台服务:定期调度任务(如清理过期数据)。
  3. 数据处理管道:将数据分阶段处理(如读取、转换、存储)。
  4. 微服务任务协调:在分布式系统中调度异步任务并处理依赖关系。

5. 常见问题与解答Q1:如何选择合适的并发数?

  • 根据资源类型决定:I/O 密集型任务(如网络请求)可以设置较高的并发数(如 10-50),CPU 密集型任务通常与 CPU 核心数相关(如 Environment.ProcessorCount)。

Q2:如何处理任务依赖?

  • 使用 await 按顺序执行依赖任务,或使用 TPL Dataflow 的 LinkTo 方法构建任务管道。

Q3:如何调试异步任务调度问题?

  • 添加详细日志,记录任务的开始、结束和异常。
  • 使用调试工具(如 Visual Studio 的并发可视化工具)分析任务执行。

Q4:TPL Dataflow 和 SemaphoreSlim 的区别?

  • SemaphoreSlim 适合简单并发控制,代码较轻量。
  • TPL Dataflow 提供更强大的功能,如数据流管道、错误传播和动态调整并发。

6. 总结C# 中的异步任务调度通过 Task、TaskScheduler、TPL Dataflow 等工具实现,结合 CancellationToken 支持取消操作。最佳实践包括:

  • 区分 I/O 和 CPU 密集型任务,选择合适的调度方式。
  • 使用 SemaphoreSlim 或 TPL Dataflow 控制并发。
  • 支持取消并妥善处理异常。
  • 避免死锁,优化性能,添加日志以便调试。

如果需要针对特定场景(如 ASP.NET Core 或高并发服务)的调度示例,或更深入的实现细节,请随时告诉我!

在 C# 中,异步取消机制的核心是通过 CancellationToken 和 CancellationTokenSource 实现协作式取消。高级用法则涉及更复杂的场景,例如组合取消源、处理超时与回调、嵌套任务取消、优雅清理资源以及在特定场景(如 ASP.NET Core 或并行任务)中的应用。以下是对 C# 异步取消机制高级用法的详细讲解,结合代码示例和最佳实践,扩展之前提到的异步编程和任务调度内容。


1. 高级取消机制的核心概念1.1 协作式取消的本质

  • 异步取消是协作式的,任务必须主动检查 CancellationToken.IsCancellationRequested 或调用 ThrowIfCancellationRequested()。
  • 高级用法需要处理复杂的取消逻辑,例如多任务协调、超时组合、回调管理等。

1.2 关键工具

  • CancellationTokenSource.CreateLinkedTokenSource:组合多个取消源,任意一个取消时触发。
  • CancellationToken.Register:注册取消时的回调,用于清理资源或记录日志。
  • CancelAfter 和超时:自动触发取消,适合超时场景。
  • TPL Dataflow 和并行任务:在复杂任务流中集成取消。
  • 异步资源清理:确保取消后资源被正确释放。

2. 高级取消用法2.1 组合多个 CancellationToken使用 CancellationTokenSource.CreateLinkedTokenSource 将多个取消源组合,任意一个取消时触发整体取消。场景:一个任务需要响应用户取消(cts1)和超时(cts2)两种取消信号。csharp

using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
using var cts1 = new CancellationTokenSource(); // 用户取消
using var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(3)); // 超时
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
var task = DoWorkAsync(linkedCts.Token);
Console.WriteLine("按任意键取消,或等待 3 秒超时...");
Console.ReadKey();
cts1.Cancel();
try
{
await task;
Console.WriteLine("任务完成");
}
catch (OperationCanceledException ex)
{
Console.WriteLine($"任务被取消:{ex.Message}");
}
}
static async Task DoWorkAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine("工作中...");
await Task.Delay(1000, cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
}
}

说明:

  • linkedCts 组合了用户取消(cts1)和超时(cts2)的信号。
  • 任意一个源取消(用户按键或 3 秒超时)都会触发任务取消。
  • linkedCts 自动管理资源,需使用 using 确保释放。

2.2 取消时的回调使用 CancellationToken.Register 注册回调,以便在取消时执行清理或日志记录。场景:任务取消时需要释放资源或记录日志。csharp

using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
using var cts = new CancellationTokenSource();
cts.Token.Register(() =>
{
Console.WriteLine("取消回调:清理资源...");
// 例如:关闭文件、释放连接等
});
var task = DoWorkAsync(cts.Token);
await Task.Delay(2000);
cts.Cancel();
try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("任务被取消");
}
}
static async Task DoWorkAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine("工作中...");
await Task.Delay(1000, cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
}
}

说明:

  • cts.Token.Register 注册的回调在 cts.Cancel() 时执行。
  • 回调适合执行清理操作,如关闭数据库连接、释放文件句柄等。
  • 多个回调可以注册到同一个 CancellationToken,按注册顺序执行。

2.3 嵌套任务的取消在嵌套的异步任务中,确保取消信号传递到所有子任务。场景:一个主任务启动多个子任务,取消主任务时需取消所有子任务。csharp

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
using var cts = new CancellationTokenSource();
var task = RunMultipleTasksAsync(cts.Token);
Console.WriteLine("按任意键取消...");
Console.ReadKey();
cts.Cancel();
try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("所有任务被取消");
}
}
static async Task RunMultipleTasksAsync(CancellationToken cancellationToken)
{
var tasks = new List
{
DoWorkAsync("子任务 1", cancellationToken),
DoWorkAsync("子任务 2", cancellationToken),
DoWorkAsync("子任务 3", cancellationToken)
};
await Task.WhenAll(tasks);
}
static async Task DoWorkAsync(string name, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"{name} 运行中 {i}");
await Task.Delay(1000, cancellationToken);
}
}
}

说明:

  • 主任务通过 Task.WhenAll 管理子任务,共享同一个 CancellationToken。
  • 取消主任务时,所有子任务都会收到取消信号并抛出 OperationCanceledException。
  • 确保子任务也检查 cancellationToken,否则取消可能被忽略。

2.4 优雅的资源清理在取消时,确保资源(如文件、网络连接)被正确释放。场景:异步读取文件,取消时确保文件流被关闭。csharp

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
using var cts = new CancellationTokenSource();
var task = ReadFileAsync("example.txt", cts.Token);
await Task.Delay(1000);
cts.Cancel();
try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("文件读取被取消");
}
}
static async Task ReadFileAsync(string path, CancellationToken cancellationToken)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
byte[] buffer = new byte[1024];
try
{
while (!cancellationToken.IsCancellationRequested)
{
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
if (bytesRead == 0) break;
Console.WriteLine($"读取 {bytesRead} 字节");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("文件流已关闭");
throw;
}
}
}

说明:

  • 使用 using 确保 FileStream 在取消后被释放。
  • ReadAsync 接受 CancellationToken,支持取消。
  • 捕获 OperationCanceledException 以记录取消状态,但仍抛出以通知调用方。

2.5 在 TPL Dataflow 中使用取消TPL Dataflow 是一个强大的异步任务调度框架,支持复杂的管道式处理和取消。场景:批量下载文件,限制并发并支持取消。csharp

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
class Program
{
static async Task Main(string[] args)
{
using var cts = new CancellationTokenSource();
var block = new ActionBlock(async url =>
{
Console.WriteLine($"开始下载 {url}");
await Task.Delay(1000, cts.Token); // 模拟下载
Console.WriteLine($"完成下载 {url}");
}, new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = 2,
CancellationToken = cts.Token // 绑定取消
});
block.Post("url1");
block.Post("url2");
block.Post("url3");
block.Complete();
Console.WriteLine("按任意键取消...");
Console.ReadKey();
cts.Cancel();
try
{
await block.Completion;
}
catch (OperationCanceledException)
{
Console.WriteLine("下载管道被取消");
}
}
}

说明:

  • ActionBlock 配置了 CancellationToken,当 cts.Cancel() 调用时,所有未完成的任务都会被取消。
  • MaxDegreeOfParallelism 限制并发数为 2。
  • block.Completion 会等待所有任务完成或取消。

2.6 异步超时与重试结合超时和重试机制,实现更健壮的取消逻辑。场景:尝试下载文件,超时后重试最多 3 次。csharp

using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var url = "example.com";
var maxRetries = 3;
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
try
{
await DownloadAsync(url, cts.Token);
Console.WriteLine("下载成功");
return;
}
catch (OperationCanceledException)
{
Console.WriteLine($"尝试 {attempt} 超时");
if (attempt == maxRetries)
{
Console.WriteLine("达到最大重试次数,放弃");
return;
}
}
}
}
static async Task DownloadAsync(string url, CancellationToken cancellationToken)
{
Console.WriteLine($"开始下载 {url}");
await Task.Delay(3000, cancellationToken); // 模拟长耗时操作
Console.WriteLine($"完成下载 {url}");
}
}

说明:

  • 每次尝试设置 2 秒超时。
  • 超时后捕获 OperationCanceledException,决定是否重试。
  • 适合网络请求等不可靠操作。

3. 最佳实践3.1 始终传递 CancellationToken

  • 确保所有异步方法接受 CancellationToken,并传递到子调用。
  • 如果方法不支持取消,传递 CancellationToken.None 作为默认值。

csharp

public async Task ProcessAsync(CancellationToken cancellationToken = default)
{
await Task.Delay(1000, cancellationToken);
}

3.2 避免忽略取消

  • 定期检查 IsCancellationRequested 或调用 ThrowIfCancellationRequested()。
  • 对于循环或长时间运行的任务,至少每秒检查一次。

3.3 正确处理资源

  • 使用 using 或 try-finally 确保资源在取消后释放。
  • 在回调中执行额外的清理逻辑。

3.4 区分取消类型

  • 使用 CancellationTokenSource.CreateLinkedTokenSource 区分不同取消源(如用户取消、超时、外部信号)。
  • 记录取消原因(如 OperationCanceledException.CancellationToken)以便调试。

3.5 优化性能

  • 避免创建过多 CancellationTokenSource,可重用未取消的实例。
  • 使用 ValueTask 减少高频取消场景中的内存分配。

3.6 测试取消逻辑

  • 编写单元测试,模拟取消场景,验证回调和资源清理。
  • 使用 CancellationTokenSource 的超时功能测试超时逻辑。

csharp

[Fact]
public async Task TestCancellation()
{
using var cts = new CancellationTokenSource(100);
await Assert.ThrowsAsync(() => DoWorkAsync(cts.Token));
}

4. 实际应用场景

  1. 分布式系统:在微服务中,取消某个服务的请求以避免级联失败。
  2. 用户交互:GUI 应用中,用户点击“取消”按钮中断长任务。
  3. 批量处理:限制并发下载,支持用户取消或超时。
  4. 管道处理:在 TPL Dataflow 管道中取消整个工作流。
  5. 资源敏感场景:在高负载系统中,取消低优先级任务以释放资源。

5. 常见问题与解答Q1:如何区分用户取消和超时取消?

  • 使用 CreateLinkedTokenSource 组合取消源,检查 OperationCanceledException.CancellationToken 的源。

Q2:取消后任务仍继续运行怎么办?

  • 确保任务内部定期检查 CancellationToken。如果任务不协作,考虑使用外部机制(如 Task.Run 的线程中断,需谨慎)。

Q3:如何在取消时保存部分进度?

  • 在 CancellationToken.Register 回调或 catch 块中保存状态。
  • 使用事务性操作(如数据库事务)确保一致性。

Q4:TPL Dataflow 如何处理取消?

  • 通过 ExecutionDataflowBlockOptions.CancellationToken 绑定取消信号,调用 Complete() 或 Fault() 终止管道。

6. 总结C# 异步取消的高级用法通过 CancellationTokenSource.CreateLinkedTokenSource、Register 回调、嵌套任务和 TPL Dataflow 等工具实现复杂场景的取消逻辑。关键点包括:

  • 组合取消源以支持多种取消条件。
  • 使用回调和 using 确保资源清理。
  • 在管道或批量任务中集成取消。
  • 结合超时和重试增强健壮性。

如果需要针对特定场景(如 ASP.NET Core 的请求取消或分布式系统)的深入示例,或更复杂的取消逻辑实现,请告诉我!

posted @ 2025-08-20 22:41  yjbjingcha  阅读(16)  评论(0)    收藏  举报