我的 C# 异步方法明明用 try-catch 包住了,里面抛的异常却直接崩了整个进程、catch 根本没拦住,我对着 async void 排查了大半天的复盘

后台服务里的事件处理逻辑,有个异步方法处理事件,我很小心地在调用处用 try-catch 包住,自以为万无一失。可生产上一旦方法内部抛异常整个进程就直接崩溃,try-catch 像不存在一样什么都没捕获到。盯着代码百思不得其解:异常明明在 try 块里抛的 catch 凭什么没接住?排查大半天才发现罪魁是个不起眼的细节——我把异步方法返回类型写成了 async void 而不是 async Task。async void 调用后立刻返回(try 块在异常抛出前就结束了),且它没有 Task 来承载异常,异常直接抛到同步上下文/线程池顶层、无人捕获、崩溃进程;而 async Task 会把异常存进 Task,await 时重新抛出、能被 try-catch 捕获。这篇从 async void vs async Task 的区别、改用 async Task 并 await/事件处理器内部自己兜底/fire-and-forget 用 Task+处理异常/用分析器揪 async void 的正解、async/await 其他常见坑(.Result死锁/忘记await吞异常/循环串行/async lambda变async void)、对比速查、异步传染性、决策图与铁律,到附上一段亲眼看 async void 异常逃逸的对比代码。核心领悟:语法糖让异步看起来像同步但掩盖了底层不同的执行模型(Task/上下文/异常传播),用同步直觉理解异步会栽跟头;享受语法糖便利不能免除理解底层的责任;对形式相近语义迥异且写错编译器不报错的相似性陷阱要用工具兜底;异步有传染性要一路await到底。

我的 C# 异步方法明明用 try-catch 包住了,里面抛的异常却直接崩了整个进程、catch 根本没拦住,我对着 async void 排查了大半天的复盘

那是我用 C# 写的一个后台服务里的事件处理逻辑。我有一个异步方法,负责处理某个事件,我很小心地在调用它的地方用 try-catch 把它包了起来,自以为万无一失。可生产上,一旦这个方法内部抛出异常,整个进程就直接崩溃了(crash),而我那个 try-catch 像不存在一样、什么都没捕获到。我盯着代码百思不得其解:异常明明在 try 块里抛出的,catch 凭什么没接住?C# 的异常处理失灵了?排查了大半天,我才发现罪魁祸首是一个极不起眼、却影响巨大的细节:我把那个异步方法的返回类型,写成了 async void 而不是 async Task。这篇就把这场"async void 吞不住异常"的事故,从头复盘一遍。

故障现场:try-catch 包住了,异常却没被捕获

先看现场。问题就藏在那个 async void 的返回类型上:

// 我的异步方法: 返回类型写成了 async void(致命!)
async void ProcessEvent(Event e)   // ✗✗ async void!
{
    await DoSomethingAsync();
    throw new Exception("处理失败");  // 抛个异常
}

// 调用处: 我以为 try-catch 能接住
try
{
    ProcessEvent(someEvent);   // 调用 async void 方法
}
catch (Exception ex)
{
    // ✗ 这里【永远不会执行】! 捕获不到 ProcessEvent 里抛的异常!
    Console.WriteLine("捕获到异常: " + ex.Message);
}
// → 结果: ProcessEvent 里的异常没被 catch 住, 直接崩溃整个进程!

// 为什么 try-catch 接不住?
// 1. async void 方法被调用时, 会"立刻返回"(它是异步的, await 处就让出了)。
//    → ProcessEvent(someEvent) 这一行【瞬间就返回了】, try 块也就结束了!
//    → 此时方法内部的 await DoSomethingAsync() 可能还没执行完。
// 2. 等方法内部真正执行到 throw 抛异常时, try-catch 早就执行完、退出了。
//    → 这个异常, 已经【脱离了那个 try-catch 的作用范围】。
// 3. 关键: async void 的异常【无处可去】!
//    - async Task: 异常会被"存进返回的 Task 里", 你 await 这个 Task 时,
//      异常会被重新抛出 → 能被 await 处的 try-catch 捕获。
//    - 但 async void: 没有返回 Task! 异常没地方存、没法被 await 接住。
//      → 它会被直接抛到"同步上下文 / 线程池"的最顶层 → 无人处理 → 崩溃进程!

// 现象拼图:
//   - async void 调用后立刻返回, try-catch 在异常抛出前就退出了。
//   - async void 没有 Task 来"承载"异常, 异常无处可去、直接崩进程。
//   - 我以为"try-catch 包住调用就安全", 但对 async void 完全无效。
//   - ★ 根因: 我用了 async void(而非 async Task), 它的异常处理方式
//     和普通异步方法完全不同 —— 异常会逃逸到顶层、崩溃进程。

看清真相后,我惊出一身冷汗。问题的根源,是我把异步方法的返回类型写成了 async void,而它的异常处理方式和正常的 async Task 完全不同。为什么 try-catch 接不住?一、async void 方法被调用时会"立刻返回"(异步,await 处就让出了),ProcessEvent(someEvent) 这行瞬间返回、try 块也就结束了,此时方法内部的 await 可能还没执行完;二、等方法内部真正执行到 throw 时,try-catch 早就退出了,异常脱离了它的作用范围;三、关键——async void 的异常无处可去async Task 的异常会被存进返回的 Task 里,你 await 它时异常会被重新抛出、能被捕获;但 async void 没有返回 Task、异常没地方存、没法被 await 接住,它会被直接抛到"同步上下文/线程池"的最顶层、无人处理、崩溃进程根因是:我用了 async void(而非 async Task),它的异常会逃逸到顶层、崩溃进程,try-catch 包住调用对它完全无效

第一件事:搞懂 async void 和 async Task 的区别

要解决它,得先彻底搞懂 async voidasync Task 的本质区别。

async void vs async Task

# 一、async Task: 正常的异步方法(应该用这个)
#   async Task DoAsync() { ... }
#   - 返回一个 Task, 调用方可以 await 它。
#   - 异常处理: 方法内的异常被"捕获并存进返回的 Task"。
#     await 这个 Task 时, 异常被重新抛出 → 能被 try-catch 捕获。
#   - 能知道"它完成了没、成功了没"(通过 Task 状态)。

# 二、async void: 特殊的、危险的异步方法(基本别用)
#   async void DoAsync() { ... }
#   - 不返回 Task(返回 void), 调用方【无法 await 它】。
#   - 异常处理: 没有 Task 来承载异常!
#     → 异常直接抛到"同步上下文(SynchronizationContext)"或线程池顶层。
#     → 通常意味着【无人捕获 → 崩溃进程】(在多数环境下)。
#   - 无法知道"它完成了没"(没有 Task 可等待/查询)。
#   - 还有: 无法组合(不能 Task.WhenAll 它)、难以测试。

# 三、async void 存在的唯一合理理由: 事件处理器(event handler)
#   - 像 button.Click += async (s, e) => {...} 这种事件处理器,
#     委托签名要求返回 void, 所以这里【不得不】用 async void。
#   - 但即使在这里, 也要在方法内部【自己 try-catch 兜住所有异常】,
#     绝不让异常逃逸出 async void 方法!

# 四、铁律:
#   - 除了"事件处理器", 异步方法【一律用 async Task / async Task】。
#   - async void 的异常, try-catch 在调用处接不住, 会崩进程。
#   - 万不得已用 async void(事件处理器), 内部必须自己 try-catch 兜底。

# 核心: async Task异常存进Task可被await处捕获; async void无Task承载异常、直接抛到顶层崩进程;
#   除事件处理器外异步方法一律用async Task; 非用async void不可则内部必须自己try-catch兜底。

想透这两者的区别,这个诡异的崩溃就清楚了。一、async Task:正常的异步方法(应该用这个)——返回一个 Task、调用方可以 await;异常被捕获并存进返回的 Task,await 时重新抛出、能被 try-catch 捕获;还能知道它完成了没、成功了没二、async void:特殊的、危险的(基本别用)——不返回 Task、调用方无法 await;没有 Task 承载异常,异常直接抛到同步上下文或线程池顶层、通常无人捕获、崩溃进程;还无法知道它完成了没、无法组合、难以测试三、async void 存在的唯一合理理由:事件处理器——button.Click += async (s,e) => {} 这种,委托签名要求返回 void,所以不得不用 async void;但即使这里,也要在方法内部自己 try-catch 兜住所有异常,绝不让异常逃逸四、铁律:除了事件处理器,异步方法一律用 async Task/async Task<T>;async void 的异常 try-catch 在调用处接不住、会崩进程;万不得已用它,内部必须自己 try-catch 兜底

第二件事:正解——改用 async Task,事件处理器内部兜底

搞懂了原理,正解就清晰了:异步方法一律用 async Task、调用方 await 它;不得不用 async void 的事件处理器,内部自己 try-catch 兜住所有异常

// ====== 正解一: 异步方法改用 async Task, 调用方 await ======
async Task ProcessEvent(Event e)   // ✓ async Task(不是 void)
{
    await DoSomethingAsync();
    throw new Exception("处理失败");
}

// 调用处: await 它, try-catch 就能接住了
try
{
    await ProcessEvent(someEvent);   // ✓ await! 异常会从这里抛出
}
catch (Exception ex)
{
    Console.WriteLine("捕获到异常: " + ex.Message);  // ✓ 现在能捕获了!
}
// → async Task 把异常存进 Task, await 时重新抛出 → try-catch 正常工作。

// ====== 正解二: 不得不用 async void 时(事件处理器), 内部自己兜底 ======
// 事件处理器签名要求返回 void, 这是 async void 唯一合理的场景:
button.Click += async (sender, e) =>   // 不得不 async void
{
    try   // ★ 必须在方法内部自己 try-catch 兜住所有异常!
    {
        await HandleClickAsync();
    }
    catch (Exception ex)
    {
        // 在这里处理/记录异常, 绝不让它逃逸出 async void!
        logger.Error(ex, "处理点击事件失败");
        ShowError("操作失败, 请重试");
    }
};
// → async void 的异常逃出去就崩进程, 所以内部必须自己接住、不让它逃逸。

// ====== 正解三: 区分"调用异步方法"的几种正确姿势 ======
// 1. 能 await 就 await(最常见):
await SomeAsyncMethod();

// 2. 不想等它完成(fire-and-forget), 但仍要处理异常:
//    ✗ 别直接 SomeAsyncMethod(); (异常没人管, 且async void会崩)
//    ✓ 用 Task 并显式处理异常:
_ = SomeAsyncMethod().ContinueWith(t =>
    { if (t.Exception != null) logger.Error(t.Exception, "后台任务失败"); },
    TaskContinuationOptions.OnlyOnFaulted);
//    或封装一个安全的 fire-and-forget 帮助方法(见附录)。

// ====== 正解四: 用工具/分析器揪出 async void ======
// - 编译器警告 / Roslyn 分析器(如 Microsoft.VisualStudio.Threading.Analyzers)
//   能检测出"不该用 async void 的地方"。
// - 代码审查时, 看到 async void(非事件处理器), 一律打回。

// ====== 正解五: 别用 async void 来"省一个 Task" ======
// 有人为了"调用方不用 await"而用 async void, 这是错的!
// → 想 fire-and-forget, 返回 Task 然后调用方用 _ = method() 或安全封装,
//   而不是用 async void(它的异常会崩进程)。

// 核心: 异步方法一律用 async Task 并 await(异常可捕获); 仅事件处理器用 async void
//   且内部必须自己 try-catch 兜底; fire-and-forget 也用 Task+显式处理异常, 别用 async void。

修复的核心,是"把异步方法的返回类型改对(async Task),并在不得不用 async void 时内部自己兜底"正解一:异步方法改用 async Task、调用方 await——async Task 把异常存进 Task,await 时重新抛出,try-catch 就能正常接住了正解二:不得不用 async void 时(事件处理器),内部自己兜底——在方法内部用 try-catch 兜住所有异常、处理/记录它,绝不让它逃逸出 async void(逃出去就崩进程)正解三:区分"调用异步方法"的正确姿势——能 await 就 await;fire-and-forget(不等它完成)也要用 Task + 显式处理异常(ContinueWith 处理 faulted),别直接裸调用正解四:用工具揪出 async void——Roslyn 分析器能检测,代码审查看到非事件处理器的 async void 一律打回正解五:别用 async void 来"省一个 Task"——想 fire-and-forget 就返回 Task 让调用方 _ = method() 或安全封装,别用 async void(异常会崩进程)归根结底:异步方法一律用 async Task 并 await(异常可捕获);仅事件处理器用 async void 且内部必须自己兜底;fire-and-forget 也用 Task+显式处理异常。

第三件事:async/await 的其他常见坑

排查后我把 async/await 的其他常见坑也系统梳理了一遍,它们一样隐蔽。

async/await 的常见坑

# 1. async void(本文): 异常逃逸崩进程。→ 用 async Task。

# 2. .Result / .Wait() 同步阻塞异步 → 死锁(见.Result死锁篇)
#    - 在有同步上下文的环境(老ASP.NET/WinForm)同步等待异步, 易死锁。
#    - → 一路 async/await 到底, 别用 .Result/.Wait()。

# 3. 忘记 await(返回的 Task 没 await)
#    DoAsync();   // ✗ 没 await! 方法可能还没执行完就往下走了,
#                 //   且 Task 里的异常没人管(被吞掉)。
#    → 要么 await, 要么显式 _ = DoAsync() 并处理异常。编译器通常会警告。

# 4. 在循环里 await(串行了, 性能差)
#    foreach (var x in list) await ProcessAsync(x);  // 串行, 一个个来
#    → 想并发: var tasks = list.Select(ProcessAsync); await Task.WhenAll(tasks);

# 5. ConfigureAwait(false)
#    - 库代码里, await 后不需要回到原同步上下文, 用 ConfigureAwait(false)
#      避免不必要的上下文切换、也能减少死锁风险。

# 6. async lambda 传给只接受 Action 的方法 → 变成 async void!
#    list.ForEach(async x => await DoAsync(x));  // ✗ 这是 async void! 异常逃逸
#    → 用 foreach + await, 或 Task.WhenAll。

# 共同根源: async/await 让异步代码"看起来像同步", 但它底层的执行模型
#   (Task、上下文、异常传播)和同步代码完全不同; 不理解这层, 就会踩坑。

# 核心: async/await常见坑有 async void异常逃逸、.Result死锁、忘记await吞异常、循环await串行、
#   async lambda变async void; 根源是它看起来像同步但底层执行模型不同, 要理解Task和异常传播。

排查让我把 async/await 的常见坑系统梳理了一遍。一、async void(本文):异常逃逸崩进程(用 async Task)。二、.Result/.Wait() 同步阻塞异步 → 死锁(一路 async/await 到底)。三、忘记 await(Task 没 await)——方法可能没执行完就往下走、且 Task 里的异常被吞掉四、在循环里 await(串行了,性能差)——想并发用 Task.WhenAll五、ConfigureAwait(false)(库代码里避免不必要的上下文切换、减少死锁)。六、async lambda 传给只接受 Action 的方法 → 变成 async void!(list.ForEach(async x => ...) 是 async void、异常逃逸,用 foreach+await 或 WhenAll)。它们的共同根源是:async/await 让异步代码"看起来像同步",但它底层的执行模型(Task、上下文、异常传播)和同步代码完全不同;不理解这层就会踩坑下面这张图,是这次 async void 吞不住异常的成因与解法:

第四件事:async void vs async Task 对比速查

这次踩坑后,我把两者的区别整理成一张表,写异步方法时一眼就知道该用哪个。

维度 async void async Task
能 await 吗 ✗ 不能 ✓ 能
异常处理 逃逸到顶层,崩进程 存进Task,await处可捕获
知道完成了吗 ✗ 无法知道 ✓ Task状态可查
能组合吗(WhenAll) ✗ 不能 ✓ 能
可测试性 差(无法等待)
适用 仅事件处理器(且内部兜底) 所有其他异步方法

这张表,把 async voidasync Task 的差距钉死了:除了"事件处理器"这个不得已的特例,async void 在每个维度上都比 async Task 差,尤其是"异常会崩进程"这一条,堪称致命它给我的最大启发是:async voidasync Task,只差一个返回类型(void vs Task),写起来几乎一模一样,可它们的行为(尤其是异常处理)却天差地别这种"代码上只差一点点、行为上天差地别"的特性,是最容易坑人的:因为它们看起来太像了,你很容易"手一滑"或"不假思索"地写错(把 Task 写成 void),而编译器还不会报错(async void 是合法的),于是这个致命的错误就悄无声息地潜伏下来,直到生产上某次异常,才以"崩溃进程"的剧烈方式爆发。这让我领悟到一个写代码时的警觉:对那些"形式相近、语义迥异、且写错了编译器不报错"的地方,要格外小心(== vs =async void vs async TaskList 的各种实现……);这些地方,是 bug 最爱潜伏的"相似性陷阱"应对之道,就是用工具(分析器、Lint)把这些"易错的相似"自动揪出来,而不是指望自己每次都不手滑。

第五件事:异步编程的"传染性"与正确姿势

这次也让我重新梳理了异步编程的一些基本原则。我把它整理了一下。

原则 错误做法 正确做法
返回类型 async void(非事件处理器) async Task / Task<T>
调用异步 .Result / .Wait() 同步等 await 一路到底
不等待结果 裸调用不处理异常 fire-and-forget 显式处理异常
循环异步 循环里串行 await 需要并发用 Task.WhenAll
异常处理 以为 try-catch 万能 理解异步异常如何传播
库代码 不加 ConfigureAwait 库里 ConfigureAwait(false)

这张表,把异步编程的正确姿势串了起来。其中一条贯穿性的原则是"async/await 一路到底":一旦一个方法是异步的,调用它的方法通常也要是异步的(用 await),不要中途用 .Result/.Wait() 把它"变回同步"(那会死锁)这就是异步的"传染性":async 会沿着调用链"向上传染"——一个底层方法异步了,它的所有上层调用者往往也得跟着异步它给我的最大启发是:异步编程不是"把某个方法加个 async"的局部改动,而是一种"会影响整条调用链"的编程范式;要么"一路异步到底"(从最底层 await 到最顶层),要么就别用,最忌讳"异步和同步混用、在中间用 .Result 强行转换"(那是死锁和各种诡异问题的温床)这让我领悟到:引入一个像"异步"这样的范式,要有"全局视角"——它不是一个孤立的局部技巧,而是会渗透并改变整个代码结构的东西;理解它的"传染性"和"全局影响",才能用对它,而不是在局部图方便、却在整体上埋下隐患对一个范式,既要会用它的"招式"(await),更要理解它的"心法"(传染性、异常传播、一路到底)——这才是真正掌握它。

第六件事:写异步方法时,我现在的检查习惯

现在每当我写一个异步方法,我都会按这张图先确认它的返回类型和异常处理:

这张图的精髓,是"写异步方法前,先定对返回类型、想清异常去哪"第一问 "它是事件处理器吗":不是就用 async Task(绝不用 async void);是(委托要求 void)就不得不 async void,但内部必须自己 try-catch 兜住所有异常、绝不让它逃逸用 async Task 时:需要结果就 await 它(调用方也跟着变异步、一路 await 到底);fire-and-forget 也用 Task + 显式处理异常,别裸调用最后开 Roslyn 分析器揪出误用的 async void这套习惯,让我写异步方法时,从"随手 async 一下"变成了"先定对返回类型、想清异常去向"——核心始终是:异步方法一律用 async Task(异常可捕获),只有事件处理器用 async void 且内部必须自己兜底。

我立下的几条规矩

这场"async void 吞不住异常"的事故,换来了我写 C# 异步时,刻进骨子里的几条铁律:

  1. 异步方法一律用 async Task,绝不用 async void。除非是不得不返回 void 的事件处理器。
  2. async void 的异常 try-catch 在调用处接不住,会崩进程。它没有 Task 承载异常。
  3. 事件处理器用 async void,内部必须自己 try-catch 兜底。绝不让异常逃逸出去。
  4. 能 await 就 await,异常才能被正常捕获。async Task 把异常存进 Task,await 时抛出。
  5. fire-and-forget 也要处理异常。用 Task + ContinueWith,别裸调用让异常被吞。
  6. async lambda 传给 Action 会变成 async void。list.ForEach(async ...) 是坑,用 foreach/WhenAll。
  7. 用 Roslyn 分析器揪 async void。这类"写错编译器不报错"的坑,靠工具兜底。

附:一段能"看见"async void 异常逃逸的对比代码

口说无凭。下面用一段对比代码,让你亲眼看到 async void 的异常逃逸、和 async Task 的异常可捕获:

using System;
using System.Threading.Tasks;

class AsyncVoidDemo
{
    // ✗ async void: 异常逃逸
    static async void ThrowVoid()
    {
        await Task.Delay(100);
        throw new Exception("来自 async void 的异常");
    }

    // ✓ async Task: 异常存进Task
    static async Task ThrowTask()
    {
        await Task.Delay(100);
        throw new Exception("来自 async Task 的异常");
    }

    static async Task Main()
    {
        // ====== 实验1: try-catch 包住 async void —— 接不住! ======
        Console.WriteLine("--- async void ---");
        try
        {
            ThrowVoid();              // 立刻返回, try块结束
            await Task.Delay(300);    // 等它内部抛异常(此时已脱离try)
        }
        catch (Exception ex)
        {
            // ✗ 这里捕获不到! (异常已逃逸到顶层)
            Console.WriteLine("void 捕获到: " + ex.Message);
        }
        Console.WriteLine("(上面没打印'void 捕获到', 说明没捕获到! "
                          + "真实环境里这个异常会崩溃进程)");

        // ====== 实验2: await async Task —— 能接住! ======
        Console.WriteLine("\n--- async Task ---");
        try
        {
            await ThrowTask();        // ✓ await它, 异常会从这里抛出
        }
        catch (Exception ex)
        {
            // ✓ 这里能捕获到!
            Console.WriteLine("task 捕获到: " + ex.Message);
        }

        // ====== 演示: 兜底未捕获异常(看 async void 异常真的逃逸了)======
        AppDomain.CurrentDomain.UnhandledException += (s, e) =>
            Console.WriteLine("!!! 未捕获异常(进程将崩溃): "
                              + ((Exception)e.ExceptionObject).Message);
        TaskScheduler.UnobservedTaskException += (s, e) =>
            Console.WriteLine("!!! 未观察的Task异常: " + e.Exception.Message);
    }
}

/* 输出(关键对比):
   --- async void ---
   (上面没打印'void 捕获到', 说明没捕获到!...)    ← try-catch 接不住!
   --- async Task ---
   task 捕获到: 来自 async Task 的异常             ← await 能接住!
   (在真实长生命周期进程里, async void 的那个异常会通过 UnhandledException
    冒到顶层, 最终崩溃进程)
*/

// 核心: 同样是抛异常, async void 的 try-catch 接不住(异常逃逸到顶层崩进程),
//   async Task 的 await + try-catch 能接住; 跑一遍, async void 的危险一目了然。

这段对比代码,把"async void 的异常逃逸"这个平时难以直观感受的危险,变成了可以亲眼对比的现象。它把 async voidasync Task 放在一起,用同样的"抛异常 + try-catch"去对待:你会清楚地看到,async Task 那边打印了"task 捕获到: ..."(异常被 await + try-catch 正常接住),而 async void 那边,那句"void 捕获到"根本没打印(异常逃逸了、try-catch 形同虚设),在真实的长生命周期进程里,这个逃逸的异常最终会通过 UnhandledException 冒到顶层、崩溃进程这,正是我想用这段代码,留给每个 C# 开发者的最后一课:对于像 async void 这种"写法上只差一个字、危险性却天壤之别、且平时很难直观感受到"的坑,最好的认知方式,就是亲手写一段对比代码,让"异常逃逸"这个抽象的危险,以"那句话没被打印出来"的具体形式,展现在你眼前当你亲眼见证"try-catch 明明包着,却什么都没捕获到"时,"async void 会让异常逃逸"这件事,就会以一种带着后怕的、难以磨灭的方式,刻进你的记忆。把"看不见的危险",通过实验变成"看得见的现象"——这是我这一系列复盘里反复验证的、对付各种隐蔽陷阱最可靠的学习法对那些"平时藏得很深、一旦爆发后果严重"的坑,与其等它在生产里以"崩溃进程"的方式教训你,不如在测试台上,用一段小小的对比实验,先把它的真面目看个清清楚楚。

写在最后

回头看,这场由 async void 引发的、try-catch 失灵进程崩溃的事故,真正教给我的,远不止"异步方法用 async Task 别用 async void"这一个知识点。它让我对"语法糖"和"底层机制"之间的关系,有了又一次深刻的体会。async/await 是 C# 一个极其优秀的语法糖,它让我们能用"近乎同步"的、直观的方式,写出异步的代码——这极大地降低了异步编程的门槛。可这次事故让我意识到:正是这种"让异步看起来像同步"的便利,也悄悄地掩盖了异步底层那套和同步完全不同的执行模型(Task、上下文、异常传播);而当我用"同步的直觉"(以为 try-catch 能包住一切)去理解异步代码时,就在 async void 这个"底层机制与同步差异最大"的地方,栽了跟头这让我领悟到一个使用一切"高级抽象/语法糖"时都至关重要的道理:语法糖让我们"用起来很爽、很简单",但它不能免除我们"理解它底层到底在做什么"的责任;尤其是当"表层的简单"和"底层的复杂"在某些边界上发生剧烈分歧时(如 async void 的异常处理),只有真正理解了底层机制的人,才能避开那些"语法糖掩盖起来的陷阱"所以,我现在用任何语法糖/高级特性时,都会提醒自己:"它让我少写了什么、又替我做了什么?它底层的真实行为,和我'看起来'的样子,在哪些地方可能不一致?"——享受语法糖的便利,但不被它的便利麻痹、始终对它底层的机制保持理解和敬畏。看穿语法糖、理解底层执行模型——这,是我用一次"async void 崩进程"的事故,换来的、关于 C#、也关于"语法糖与底层机制"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次写异步方法时,条件反射地把返回类型写成 async Task,那我对着那个 try-catch 拦不住的崩溃熬的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我用 Redis 加了分布式锁防并发,结果两个进程还是同时执行了、还互相把对方的锁删了,我对着分布式锁的几个致命细节排查了大半天的复盘

2026-6-2 8:03:40

技术教程

我用数字枚举判断用户角色,结果第一个角色 Admin 怎么判都是"假"、权限全乱了,我对着 TypeScript 数字枚举的几个坑排查了大半天的复盘

2026-6-2 8:15:54

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索