编辑
2026-01-01
C#
00

目录

🚫 避免async void:异步编程的第一原则
❌ async void的致命问题
✅ 正确的异步方法设计
🛑 尊重CancellationToken:让异步操作可控
💡 为什么需要CancellationToken?
⚡ ConfigureAwait(false):库代码的性能优化
🎯 理解同步上下文的性能影响
📋 ConfigureAwait使用规则
🎯 实战总结

作为C#开发者,你是否写过async void方法却不知道隐患?是否忽略了CancellationToken参数导致无法取消长时间运行的操作?在构建高性能应用时,这些看似细微的异步编程细节,往往决定了系统的稳定性和可维护性。

今天分享3个异步编程的专业级技巧,这些都是我在生产环境中踩坑总结的宝贵经验。掌握这些技巧,不仅能避免常见的异步陷阱,还能让你的代码在性能和可靠性方面更上一层楼。

让我们深入探索如何写出真正专业的异步代码!

🚫 避免async void:异步编程的第一原则

❌ async void的致命问题

核心问题: 无法捕获异常,无法等待完成,调用者失去控制权

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, "直接获取用户时发生错误"); } } } }

image.png

唯一例外: 事件处理程序可以使用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); } }

🛑 尊重CancellationToken:让异步操作可控

💡 为什么需要CancellationToken?

解决的核心问题: 长时间运行的操作需要能够被取消,避免资源浪费,既然用了异步,取消尽量不要少了,不过我实际中可以不会都写上。

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(); } } } }

image.png

⚡ ConfigureAwait(false):库代码的性能优化

🎯 理解同步上下文的性能影响

核心问题: 在库代码中,不必要的上下文切换会影响性能

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(); } } }

image.png

📋 ConfigureAwait使用规则

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 许可协议。转载请注明出处!