作为C#开发者,你是否写过async void方法却不知道隐患?是否忽略了CancellationToken参数导致无法取消长时间运行的操作?在构建高性能应用时,这些看似细微的异步编程细节,往往决定了系统的稳定性和可维护性。
今天分享3个异步编程的专业级技巧,这些都是我在生产环境中踩坑总结的宝贵经验。掌握这些技巧,不仅能避免常见的异步陷阱,还能让你的代码在性能和可靠性方面更上一层楼。
让我们深入探索如何写出真正专业的异步代码!
核心问题: 无法捕获异常,无法等待完成,调用者失去控制权
c#// ❌ 危险写法:异常会导致程序崩溃
public async void ProcessDataDangerous()
{
await Task.Delay(1000);
throw new InvalidOperationException("出错了!"); // 无法被捕获!
}
// ❌ 调用者无法等待完成
public void BadCaller()
{
ProcessDataDangerous(); // 无法知道何时完成
// 可能在操作完成前就继续执行
}
c#using System;
using System.Threading.Tasks;
namespace AppAsync3
{
// 自定义异常
public class UserNotFoundException : Exception
{
public UserNotFoundException(string message) : base(message) { }
}
// DTO
public class UserProfile
{
public string Name { get; }
public string Email { get; }
public UserProfile(string name, string email)
{
Name = name;
Email = email;
}
}
// 用户实体
public class User
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
// 仓储接口
public interface IUserRepository
{
Task<User?> FindByIdAsync(Guid userId);
}
// 实现中的一个简单仓储
public class InMemoryUserRepository : IUserRepository
{
// 简单示例数据
private readonly System.Collections.Concurrent.ConcurrentDictionary<Guid, User> _store =
new System.Collections.Concurrent.ConcurrentDictionary<Guid, User>();
public InMemoryUserRepository()
{
// 初始化一个示例用户
var id = Guid.NewGuid();
_store[id] = new User { Id = id, Name = "张三", Email = "zhangsan@example.com" };
// 将一个已知的 ID 暴露出来,便于测试 GetUserProfileAsync
KnownId = id;
}
// 暴露一个已知的存在的 ID,用于测试
public Guid KnownId { get; }
public Task<User?> FindByIdAsync(Guid userId)
{
_store.TryGetValue(userId, out var user);
return Task.FromResult<User?>(user);
}
}
// 日志接口
public interface ILogger
{
void LogError(Exception ex, string message);
}
// 简单控制台日志实现
public class ConsoleLogger : ILogger
{
public void LogError(Exception ex, string message)
{
Console.WriteLine($"ERROR: {message} - {ex}");
}
}
public class DataProcessor
{
private readonly IUserRepository _userRepository;
private readonly ILogger _logger;
public DataProcessor(IUserRepository userRepository, ILogger logger)
{
_userRepository = userRepository;
_logger = logger;
}
public async Task ProcessDataSafely()
{
// 模拟异步工作
await Task.Delay(1000);
// 异常可以被正确传播和处理
throw new InvalidOperationException("出错了!");
}
public async Task<UserProfile> GetUserProfileAsync(Guid userId)
{
var user = await _userRepository.FindByIdAsync(userId);
if (user == null)
throw new UserNotFoundException($"用户 {userId} 不存在");
return new UserProfile(user.Name, user.Email);
}
public async Task SafeCaller()
{
try
{
await ProcessDataSafely();
if (_userRepository is InMemoryUserRepository inMem && inMem.KnownId != Guid.Empty)
{
var profile = await GetUserProfileAsync(inMem.KnownId);
Console.WriteLine($"User Profile: {profile.Name}, {profile.Email}");
}
else
{
var randomId = Guid.NewGuid();
var profile = await GetUserProfileAsync(randomId);
Console.WriteLine($"User Profile: {profile.Name}, {profile.Email}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "处理数据时发生错误");
}
}
}
class Program
{
public static async Task Main(string[] args)
{
// 构建依赖
IUserRepository userRepository = new InMemoryUserRepository();
ILogger logger = new ConsoleLogger();
var processor = new DataProcessor(userRepository, logger);
await processor.SafeCaller();
try
{
// 使用已知存在的用户 ID 测试成功路径
if (userRepository is InMemoryUserRepository inMem)
{
var profile = await processor.GetUserProfileAsync(inMem.KnownId);
Console.WriteLine($"直接获取的用户:{profile.Name} <{profile.Email}>");
}
// 使用一个不存在的 ID 测试异常
var nonExistId = Guid.NewGuid();
await processor.GetUserProfileAsync(nonExistId);
}
catch (Exception ex)
{
logger.LogError(ex, "直接获取用户时发生错误");
}
}
}
}

唯一例外: 事件处理程序可以使用async void
c#// ✅ 事件处理器是async void的唯一合法场景,Winform下MS好像是改不过来了
private async void Button_Click(object sender, EventArgs e)
{
try
{
await ProcessUserActionAsync();
}
catch (Exception ex)
{
ShowErrorMessage(ex.Message);
}
}
解决的核心问题: 长时间运行的操作需要能够被取消,避免资源浪费,既然用了异步,取消尽量不要少了,不过我实际中可以不会都写上。
c#using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
using var cts = new CancellationTokenSource();
Task runTask = RunLongOperationAsync(cts.Token);
// 模拟用户输入:按 Enter 取消,或等待 10 秒后自动取消
Console.WriteLine("任务正在运行。按 Enter 取消,或等待 10 秒自动取消。");
var cancelTask = Task.Run(() =>
{
Console.ReadLine();
cts.Cancel();
});
// 或者自动取消的示例
// await Task.Delay(TimeSpan.FromSeconds(5)).ContinueWith(t => cts.Cancel());
try
{
await runTask;
Console.WriteLine("任务顺利完成。");
}
catch (OperationCanceledException)
{
Console.WriteLine("任务已取消,进行了资源清理。");
}
catch (Exception ex)
{
Console.WriteLine($"任务执行错误:{ex.Message}");
}
finally
{
// 资源清理放在这里(如关闭文件、释放句柄等)
Console.WriteLine("清理完毕。");
}
}
static async Task RunLongOperationAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 100; i++)
{
// 每次循环模拟工作负载
await Task.Delay(100, cancellationToken);
// 显示进度,帮助理解取消点
Console.WriteLine($"进度: {i + 1} / 100");
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("检测到取消请求,准备退出。");
cancellationToken.ThrowIfCancellationRequested();
}
}
}
}

核心问题: 在库代码中,不必要的上下文切换会影响性能
c#using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace ContextSwitchDemo
{
public static class LibraryTaskRunner
{
// 模拟一个基础异步工作
public static async Task<int> DoWorkAsync(int input, int delayMs, CancellationToken cancellationToken)
{
await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false);
return input * 2;
}
// 实现 A:逐步串行执行,每一步都在单独的异步方法中等待
public static async Task<int> ProcessSequentialAsync(int[] inputs, CancellationToken cancellationToken)
{
int sum = 0;
foreach (var x in inputs)
{
// 每次等待一个独立的任务完成,存在一次性上下文切换
int r = await DoWorkAsync(x, 50, cancellationToken).ConfigureAwait(false);
sum += r;
}
return sum;
}
// 实现 B:批量并发执行,尽量减少上下文切换
public static async Task<int> ProcessBatchAsync(int[] inputs, CancellationToken cancellationToken)
{
var tasks = inputs
.Select(x => DoWorkAsync(x, 50, cancellationToken))
.ToArray();
int[] results = await Task.WhenAll(tasks).ConfigureAwait(false);
return results.Sum();
}
public static async Task<(long sequentialMs, long batchMs, int sequentialResult, int batchResult)> CompareImplementationsAsync(int[] inputs, CancellationToken cancellationToken)
{
// 严格控制计时,避免其他因素干扰
var swSeq = Stopwatch.StartNew();
int seq = await ProcessSequentialAsync(inputs, cancellationToken).ConfigureAwait(false);
swSeq.Stop();
var swBatch = Stopwatch.StartNew();
int batch = await ProcessBatchAsync(inputs, cancellationToken).ConfigureAwait(false);
swBatch.Stop();
return (swSeq.ElapsedMilliseconds, swBatch.ElapsedMilliseconds, seq, batch);
}
}
class Program
{
static async Task Main(string[] args)
{
int[] inputs = Enumerable.Range(1, 100).ToArray();
using var cts = new CancellationTokenSource();
Console.WriteLine("Context Switch Demo: 库实现比较 (Sequential vs Batch)");
Console.WriteLine("说明:Sequential 每步是一个独立的异步等待,Batch 尽量一次性等待全部任务,减少上下文切换。");
var (seqMs, batchMs, seqRes, batchRes) = await LibraryTaskRunner
.CompareImplementationsAsync(inputs, cts.Token);
Console.WriteLine($"Sequential: 结果={seqRes}, 时间={seqMs} ms");
Console.WriteLine($"Batch: 结果={batchRes}, 时间={batchMs} ms");
if (batchMs < seqMs)
{
Console.WriteLine("结论:Batch 实现显著减少了上下文切换,总体更快。");
}
else
{
Console.WriteLine("结论:在当前场景下,Batch 不一定总是更快,请结合实际工作负载分析。");
}
Console.WriteLine("按任意键退出...");
Console.ReadKey();
}
}
}

c#public class BestPracticesExample
{
// ✅ 库代码:始终使用ConfigureAwait(false)
public async Task<string> LibraryMethodAsync()
{
var result = await SomeAsyncOperation().ConfigureAwait(false);
var processed = await ProcessResult(result).ConfigureAwait(false);
return processed;
}
// ✅ 应用程序代码:通常不需要ConfigureAwait
private async void Button_Click(object sender, EventArgs e)
{
// UI代码需要回到UI线程,所以不使用ConfigureAwait(false)
var result = await LibraryMethodAsync();
// 可以安全地更新UI
ResultLabel.Text = result;
}
// ✅ ASP.NET Core:通常不需要ConfigureAwait
[HttpGet]
public async Task<ActionResult<string>> GetDataAsync()
{
// ASP.NET Core没有同步上下文,ConfigureAwait影响不大
// 但在库代码中仍然建议使用
var result = await LibraryMethodAsync();
return Ok(result);
}
}
- 库代码优先用 ConfigureAwait(false):别抓住调用方的上下文,让后台线程继续工作就好,省得多花上下文切换的成本。
- 需要回到调用方上下文时再说:如果后续真的要更新 UI/依赖于调用方的上下文的部分,再显式切换回来,不要一味都用 false。
- UI 与 Web 应用的取舍:UI 事件入口处可以继续保持上下文,后台操作用 false,更新 UI 的部分再回到 UI 线程。
- 避免小任务逐个 await:如果可以,把很多小任务放一起用 WhenAll 一次性等完,减少频繁的上下文切换。
- 异常与取消要管好:确保取消和异常能统一处理,不会让资源泄露或线程卡住。
掌握这三个异步编程技巧,你的代码将实现:
🛡️ 更高的可靠性: 避免async void陷阱,正确处理异常传播
🎛️ 更好的控制性: 通过CancellationToken实现优雅的操作取消
⚡ 更优的性能: ConfigureAwait(false)减少不必要的上下文切换
这些技巧体现了专业异步编程的核心理念:让异步操作既高效又可控。在我的实际项目中,严格遵循这些原则让系统在高并发场景下的表现更加稳定,也让代码维护变得更加轻松。
记住这个口诀:"库用Task,传Token,false配置" - 简单好记,受用终生!
你在异步编程中遇到过哪些坑? 是否有因为忽略CancellationToken导致的性能问题?欢迎分享你的异步编程经验和踩坑故事!
觉得这些异步技巧有用?请转发给团队同事,一起写出更专业的异步代码! 🚀
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!