别再写 async void 了!C# 高可靠异步代码的三个关键实践

48 阅读5分钟

前言

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

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

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

async void 的致命问题

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

// ❌ 危险写法:异常会导致程序崩溃
public async void ProcessDataDangerous()
{
    await Task.Delay(1000);
    throw new InvalidOperationException("出错了!"); // 无法被捕获!
}

// ❌ 调用者无法等待完成
public void BadCaller()
{
    ProcessDataDangerous(); // 无法知道何时完成
    // 可能在操作完成前就继续执行
}

正确的异步方法设计

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

// ✅ 事件处理器是async void的唯一合法场景,Winform下MS好像是改不过来了
private async void Button_Click(object sender, EventArgs e)
{
    try
    {
        await ProcessUserActionAsync();
    }
    catch (Exception ex)
    {
        ShowErrorMessage(ex.Message);
    }
}

尊重 CancellationToken:让异步操作可控

为什么需要 CancellationToken?

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

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

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

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

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

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

ConfigureAwait 使用规则

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 导致的性能问题?欢迎分享你的异步编程经验和踩坑故事!

关键词

async void、CancellationToken、ConfigureAwait、异步编程、异常处理、性能优化、上下文切换、Task、取消令牌、C#

mp.weixin.qq.com/s/4OabXsz_usSNC-ZiDaFbaA

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!