提问人:joaocarlosib 提问时间:6/14/2023 最后编辑:joaocarlosib 更新时间:6/14/2023 访问量:169
在 C# 中处理异步方法中的同步代码的正确方法是什么?
What is the correct way to deal with synchronous code inside async methods in C#?
问:
我首先会说我是 .NET 的一名大三学生,在意识到它可以提高我正在开发的应用程序的性能后,我最近开始涉足基于任务的编程。我们的环境是大多数遗留代码,我需要重用它的一系列功能。
该应用程序包括将复杂对象列表反序列化为一个大型的单个文本块,通过 http 发送字符串,执行计算,在 T-SQL 数据库中编写响应并输出一堆大型数据报告。列表中的每个对象都只有一个这样的大字符串,并且它们彼此不依赖。基于这种行为,我开始思考如何使用异步编程来改进代码。
在过去的一周里,我读了很多书,从Microsoft的官方文档开始,早餐问题,异步/等待,TAP文档,Stephen Cleary指南和MVP人员的其他常见文章。经过研究,大部分部分对我来说都非常清楚,关键字、线程和上下文、阻塞、状态机、异步 != 并行、任务建模和其他东西,但我仍然有一个很大的疑问:在 C# 中的异步方法中处理同步代码(I/O 和 CPU 绑定)的正确方法是什么?
下面我提供了之前介绍的问题的示例:
这是非阻塞 UI 上下文中的按钮
private async void btnConsultar_Click(object sender, EventArgs e) {
var list = new List<ClassObject>(data);
await Sincronizar(list, progress);
}
调用后,不应阻止 UI 上下文并开始并行执行任务
public async Task Sincronizar(List<ClassObject> list, IProgress<int> progress) {
var tasks = new List<Tasks>();
foreach (ClassObject input in list) {
\\Here is the first sync function I implement,
\\which calculates the percentage of the progress
\\It is a simple math function, not covering completion,
\\with low resource needs
\\progCalc(progress, list.Count);
tasks.Add(ProcessObject(input));
}
await Task.WhenAll(tasks);
}
所以,progCalc 是一种主要类型的基本计算方法,在我的理解中,它是受 CPU 限制的,但是我应该如何声明它呢?
声明无效并保持原样:
private void progCalc(..) --> progCalc(..)
在 Task.Run 中声明 void 和 wrap:
private void progCalc(..) --> await Task.Run(() => progCalc(..)
直接声明为 Task 并等待:
private Task progCalc(..) --> await progCalc(..)
或者也许,甚至是其他方法? 据我所知,我永远不应该将同步方法声明为异步,而只是等待最后的结果。
同样的问题也适用于下一种方法,该方法并行运行并处理遗留代码,并结合数据库 CRUD、I/O 绑定方法和大规模 PDF 文件操作。
public async Task ProcessObject(ClassObject obj) {
\\Deserialize the object into a really big string, the formatter is synchronous
string chunk = FormatStringLegacy(obj);
\\Perform asynchronous http request
\\Gets a simple response with integer status code and even bigger string
HttpResponseClass response = await ExternalHttpRequestAsync(chunk);
\\Create a different object iterating the text response, needs to be sync
OtherComplexObject foo = CreateBar(foo)
\\I/O bound operations that do not depend each other, but one is legacy
Task dbTask = InsertIntoDatabaseAsync(foo)
var file = CreateFileLegacy(foo, Extensions.PDF)
Process.Start(file)
await dbTask;
}
我脑海中有一个想法告诉我,遗留函数应该只是触发并忘记,而不是等待结果并跳到下一行,因为它们被声明为同步,但在异步块内运行。我希望有人向我解释引擎盖下的同步方法发生了什么。提前致谢。
答:
对于可以在合理时间内完成的“简单”同步方法,通常最好只以内联方式调用它们。让他们返回任务没有任何好处。但我怀疑他们是否真的应该.通常,如果要进行计算,函数应该是函数式的:将其输入作为参数,并将其输出作为返回值,而不是产生副作用。void
对于更耗时的同步方法,我仍然会让方法签名本身保持同步,但如果您需要快速响应用户(例如,您在 UI 线程上),您可以使用它们在单独的线程上运行这些方法。Task.Run()
我还建议仔细检查耗时的同步方法是否真的是同步的。例如,有一些方法可以异步执行 ed Process。现在,大多数 I/O 绑定操作都有一个版本,即使旧代码可能正在调用这些方法的同步版本。await
Start
Async
但是,作为一般规则,任何异步都应该在某种程度上进行编辑:真正的即发即弃是危险的,尤其是当可能发生错误时。如果您不需要 UI 或请求者等待该任务完成,您可以将其移动到某种后台队列中,以确保等待所有任务并记录抛出的任何错误。await
评论
async
a++;