[译] 省略 Async 与 Await

news/2025/11/12 21:27:02/文章来源:https://www.cnblogs.com/talentzemin/p/19215526

https://blog.stephencleary.com/2016/12/eliding-async-await.html

当开发者掌握了 asyncawait 的基础知识并能熟练运用后,常会遇到一个设计疑问:如果可以移除 asyncawait 关键字,是否应该这样做?在很多场景下,确实可以省略这两个关键字,直接返回任务(task)。

这个问题的答案远比想象中复杂。事实上,我现在的观点与最初对此问题的立场已有所不同。

支持省略关键字的理由

效率优势

省略 asyncawait 能提升效率。不使用这两个关键字时,编译器无需生成 async 状态机。这意味着程序集中的编译器生成类型会减少,垃圾回收器的压力会减轻,需要执行的 CPU 指令也会变少。

但需要明确的是,这些性能提升都极其微小。仅会减少一个类型、节省少量小对象的垃圾回收开销,同时跳过少量 CPU 指令。绝大多数情况下,async 用于处理 I/O 操作,而 I/O 操作的耗时会完全掩盖这些性能提升。几乎在所有场景中,省略 asyncawait 都不会对应用程序的运行时间产生实质性影响。

若想全面了解省略 asyncawait 带来的效率优势,可参考 Stephen Toub 的经典视频《异步编程的禅道》(The Zen of Async)或他在 MSDN 上的相关文章。

我刚开始撰写关于 async 的内容时,总会建议省略 asyncawait,但近年来已调整了这一立场。省略关键字存在太多潜在陷阱,不应将其作为默认选择。如今,除少数特定场景外,我都建议保留 asyncawait 关键字——以下将详细说明其中的弊端。

潜在陷阱

省略 asyncawait 后,编译器不会对方法进行相关修改。但这也意味着,若想保持相同的语义,原本由编译器完成的工作都需要手动实现。

using 语句的问题

省略 asyncawait 时最常见的错误之一,是开发者忘记方法末尾有代码需要在合适的时机执行。尤其是在使用 using 语句的场景中:

public async Task<string> GetWithKeywordsAsync(string url)
{using (var client = new HttpClient())return await client.GetStringAsync(url);
}public Task<string> GetElidingKeywordsAsync(string url)
{using (var client = new HttpClient())return client.GetStringAsync(url);
}

在这个示例中,省略关键字会导致下载操作中止。

我们可以通过梳理代码执行流程来理解这一点(若需回顾相关知识,我撰写的 asyncawait 入门文章至今仍适用)。为简化说明,假设 HttpClient.GetStringAsync 永远不会同步完成。

对于 GetWithKeywordsAsync,代码执行流程如下:

  1. 创建 HttpClient 对象。
  2. 调用 GetStringAsync,返回一个未完成的任务。
  3. 暂停方法执行,直到 GetStringAsync 返回的任务完成,并返回一个未完成的任务。
  4. GetStringAsync 返回的任务完成后,恢复方法执行。
  5. 释放 HttpClient 对象。
  6. 完成 GetWithKeywordsAsync 之前返回的任务。

对于 GetElidingKeywordsAsync,代码执行流程如下:

  1. 创建 HttpClient 对象。
  2. 调用 GetStringAsync,返回一个未完成的任务。
  3. 释放 HttpClient 对象。
  4. 返回 GetStringAsync 生成的任务。

显然,HttpClient 在 GET 任务完成前就被释放了,这会导致请求被取消。正确的做法是(异步)等待 GET 操作完成后,再释放 HttpClient——而使用 asyncawait 恰好能实现这一逻辑。

异常处理的差异

另一个容易被忽略的陷阱与异常相关。async 方法的状态机会捕获代码中的异常,并将其封装到返回的任务中。若没有 async 关键字,异常会直接抛出,而非被封装到任务里:

public async Task<string> GetWithKeywordsAsync()
{string url = /* 可能抛出异常的逻辑 */;return await DownloadStringAsync(url);
}public Task<string> GetElidingKeywordsAsync()
{string url = /* 可能抛出异常的逻辑 */;return DownloadStringAsync(url);
}

只要调用方采用以下方式调用,这两个方法的行为完全一致:

var result = await GetWithKeywordsAsync(); // 正常工作
var result = await GetElidingKeywordsAsync(); // 正常工作

但如果方法调用与 await 操作分离,两者的语义就会出现差异:

var task = GetWithKeywordsAsync();
var result = await task; // 异常在此处抛出var task = GetElidingKeywordsAsync(); // 异常在此处抛出
var result = await task;

方法调用与 await 分离的场景有很多。例如,调用方可能在执行异步操作的同时,还需要处理其他工作——这种情况在使用 Task.WhenAll 的代码中最为常见。

异步编程的预期语义是,异常应被封装到返回的任务中。因为返回的任务代表了方法的执行过程,若方法执行因异常终止,最自然的表现形式就是一个出错的任务(faulted task)。

因此,在这种场景下省略关键字,会导致异常行为与预期不符。

AsyncLocal 的上下文问题

这个陷阱的逻辑相对复杂。

AsyncLocal<T>(以及更低层级的 LogicalCallContext)允许异步代码使用一种与 async 兼容的、类似线程本地存储(thread local storage)的机制。其工作原理是,在 async 编译器转换过程中,编译器生成的代码会通知逻辑调用上下文(logical call context),建立一个写时复制(copy-on-write)作用域。

这一机制能让上下文信息在异步调用中“向下”传递,但需要注意的是,值不会“向上”传递。

static AsyncLocal<int> context = new AsyncLocal<int>();static async Task MainAsync()
{context.Value = 1;Console.WriteLine("Should be 1: " + context.Value);await Async();Console.WriteLine("Should be 1: " + context.Value);
}static async Task Async()
{Console.WriteLine("Should be 1: " + context.Value);context.Value = 2;Console.WriteLine("Should be 2: " + context.Value);await Task.Yield();Console.WriteLine("Should be 2: " + context.Value);
}

在上述示例中,子方法 Async 中修改了上下文值,但当 Async 完成且控制流回到 MainAsync 后,代码会在“父”上下文环境中执行。因此,“父”上下文的值会传递到“子”上下文,但“子”上下文修改后的值不会传递回“父”上下文。

需要注意的是,同步方法不会通知逻辑调用上下文发生了变化。对于普通的(非返回任务的)同步方法,这不会有问题——从逻辑调用上下文的角度来看,所有同步调用都会被“合并”,它们实际上属于调用栈中最近的上层 async 方法的上下文。例如:

static AsyncLocal<int> context = new AsyncLocal<int>();static async Task MainAsync()
{context.Value = 1;Console.WriteLine("Should be 1: " + context.Value);await Async();Console.WriteLine("Should be 1: " + context.Value);
}static async Task Async()
{Console.WriteLine("Should be 1: " + context.Value);Sync();Console.WriteLine("Should be 2: " + context.Value);await Task.Yield();Console.WriteLine("Should be 2: " + context.Value);
}static void Sync()
{Console.WriteLine("Should be 1: " + context.Value);context.Value = 2;Console.WriteLine("Should be 2: " + context.Value);
}

在这个示例中,Async 方法能感知到其子方法 Sync 对上下文的修改。如前所述,我更倾向于将同步方法理解为上层最近的 async 上下文的一部分——从上下文角度来看,Sync 只是 Async 的一部分。

而当省略 asyncawait 时,返回任务的非 async 方法会被上下文视为普通同步方法。因此,若该方法修改了逻辑调用上下文,实际上会影响其上层上下文:

static AsyncLocal<int> context = new AsyncLocal<int>();static async Task MainAsync()
{context.Value = 1;Console.WriteLine("Should be 1: " + context.Value);await Async();Console.WriteLine("Should be 1: " + context.Value); // 实际输出为 "2"——与预期不符!
}static Task Async()
{Console.WriteLine("Should be 1: " + context.Value);context.Value = 2;Console.WriteLine("Should be 2: " + context.Value);return Task.CompletedTask;
}

这种场景并不常见,但确实是省略 async/await 可能引发的陷阱之一。

推荐指南

我建议遵循以下准则:

  • 默认不省略关键字。使用 asyncawait 能让代码更自然、更易读。
  • 当方法仅作为透传(passthrough)或重载时,可考虑省略关键字。

示例:

// 简单透传到下一层:省略关键字
Task<string> PassthroughAsync(int x) => _service.PassthroughAsync(x);// 方法的简单重载:省略关键字
async Task<string> OverloadsAsync(CancellationToken cancellationToken)
{... // 核心实现,使用 await
}
Task<string> OverloadsAsync() => OverloadsAsync(CancellationToken.None);// 非简单透传:使用关键字
async Task<string> PassthroughAsync(int x)
{// 原因:GetFirstArgument 可能抛出异常// 即使现在不抛异常,后续也可能有人修改它,且未必会同步修改此方法return await _service.PassthroughAsync(GetFirstArgument(), x);
}// 非简单方法重载:使用关键字
async Task<string> OverloadsAsync()
{// 原因同上:GetDefaultCancellationTokenForThisScope 可能抛出异常return await OverloadsAsync(GetDefaultCancellationTokenForThisScope());
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/963783.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

你的代码正在腐烂!你的团队正走在死亡螺旋上:技术债务积累的5个危险信号!

本文深度解析技术债务管理之道,指出技术债务需合理管理而非完全消除,介绍了技术债务的四大类型(设计、代码、测试、文档),以及识别评估模型与偿还策略,并提供了实战案例与预防措施。关注我,掌握企业数字化/信息…

iverilog、gtkwave工具链接

最近在尝试TRAE生成代码,工具生成代码后会检查系统中是否有仿真工具,安装iverilog+gtkwave,方便TRAE调用检查生成的代码。 链接:https://bleyer.org/icarus/****************************************************…

2025 11 12

某NOIP模拟赛T1 一个整除分块类似的东西直接秒了 T2考虑 \([l,r]\) 的交换操作可以看成 \([l,m]\) 和 \([m,r+1]\),故我们可以对这个进行处理即可第23场弘文了这场我T1都没切,我考虑枚举排列和起点之后,我不知道为什…

使用WiX创建Windows应用安装包 - -YADA

参考:官方教程 WiX 工具集(简称 WiX)用于构建 Windows 安装程序,它是构建工具、运行时工具和库的集合,不只是制作基本的安装包,还可以安装IIS网站、创建SQL Server、在Windows防火墙中注册例外。 安装 Wix 工具集…

学生信息管理系统团队项目随笔

一、团队基本信息这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13481…

Total Recall: 如何在Windows下开发输入法

https://github.com/KunYi/W2K_DDK_SAMPLES/archive/refs/heads/main.zip 链接: https://pan.baidu.com/s/1Dw4SZSZUY-lrAiBWTtP_5w 提取码: enjcW2K_DDK_SAMPLES-main/ime/chs下的文件: candd.bmp canddp.bmp cande.…

大数据量场景下的编辑 / 选择 / 详情优化

面对企业系统中主子表页面的卡顿难题,需以全链路“按需”设计破局:通过前端差异提交、后端批量处理与数据层协同,将性能优化内化为无缝的用户体验,让海量数据操作变得举重若轻。在企业级系统开发中,最容易卡顿、超…

简化Python数据结构初始化:从繁琐到优雅的进阶指南 - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

RabbitMQ相关

RabbitMQ的AMQP协议是什么 AMQP(Advanced Message Queuing Protocol),高级消息队列协议,提供统一消息服务的开放标准,其核心目标是实现客户端与消息服务之前的高效、安全异步通信,并且在传递的时候不受客户端和开发…

第八天 测试用例编写

一、微信发红包xmind图二、高效编写测试用例的实用技巧 1、用例的来源:公司模板、自己设计、用例管理工具(如禅道) 2、核心字段:用例编号、用例标题、前置条件、用例步骤、预期结果、优先级 3、注意点: (1)用例…

软工团队作业2--需求规格说明书

作业信息这个作业属于哪个课程 首页 - 计科23级34班 - 广东工业大学 - 班级博客 - 博客园这个作业要求在哪里 团队作业2-《需求规格说明书》 - 作业 - 计科23级34班 - 班级博客 - 博客园这个作业的目标 明确团队项目细…

没用的博客园页面的要素介绍

rt1. 关于那几行字点击查看"<b style=color:rgb(119, 248, 255)>又是一年雨季</b>","<b style=color:rgb(119, 248, 255)>青苔悄悄爬满缝隙</b>","<b style=colo…

使用NVIDIA TAO 6和DeepStream 8构建实时视觉检测管道 - 实践

使用NVIDIA TAO 6和DeepStream 8构建实时视觉检测管道 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Cons…

ChatBI 重构工业数据交互:TDengine IDMP 让数据对话更智能

在工业数据处理领域,传统的 BI 工具往往需要用户具备专业的数据分析技能,通过复杂的操作才能获取所需信息,这在快节奏的工业生产中显得效率低下。而 ChatBI 的出现,正以自然语言交互为核心,为工业数据交互带来了革…

结婚证识别科技:利用OCR和深度学习实现婚姻证件信息的自动提取与结构化处理

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

BOE(京东方)荣获第四届“纪念彼得德鲁克中国管理奖” 创新管理模式获权威认可

11月11日,“2025纪念彼得德鲁克中国管理论坛”在南京顺利召开。BOE(京东方)凭借其融合战略引领、创新驱动、卓越运营与文化支撑为一体的创新管理模式,在长期产业实践中成效卓著,并从近百家参选企业中脱颖而出,荣…

云服务模式进化论:企业云战略的致命误区,从IaaS到FaaS的死亡之旅!

本文深度解析云计算五大服务模式(IaaS、PaaS、SaaS、BaaS、FaaS)的技术本质、商业价值与落地实践,指出没有最好的模式,只有最适合企业业务场景和发展阶段的选择,并分享了不同规模企业的选型策略与转型经验。关注我…

青少年电子设计比赛培训笔记3

初识图形化编程 Mixly软件使用 软件下载软件下载:https://pan.baidu.com/share/init?surl=s0Xl2JiUeMnvZsb452maqQ?pwd=nm35 需要下载并安装驱动程序和编程软件软件使用介绍 软件界面:开发板连接及程序烧录使用Typ…

#题解#洛谷P1314#二分#前缀和#

[传送门](P1314 [NOIP 2011 提高组] 聪明的质监员 - 洛谷) 分析 1.W变大,则要求条件更严格,则sigema(y)不增,具有单调性,考虑二分查找W。O(log w) 2.对于每一个W,可以处理前缀和求特征值。O(n+m) 3.总时间复杂…

Python 实现对遥感影像根据DN值上色

Python 实现对遥感影像根据DN值上色import os import re import glob import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt import rasterio from rasterio.plot import plotting_extent fr…