在 C# 中处理异步方法中的同步代码的正确方法是什么?

What is the correct way to deal with synchronous code inside async methods in C#?

提问人:joaocarlosib 提问时间:6/14/2023 最后编辑:joaocarlosib 更新时间:6/14/2023 访问量:169

问:

我首先会说我是 .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;

    }

我脑海中有一个想法告诉我,遗留函数应该只是触发并忘记,而不是等待结果并跳到下一行,因为它们被声明为同步,但在异步块内运行。我希望有人向我解释引擎盖下的同步方法发生了什么。提前致谢。

c# .net 异步 async-await task-parallel-library

评论

1赞 Poul Bak 6/14/2023
所有方法都有 synchron 方法,例如是 synchron 方法。这没有错。如果任务是次要的,那么只需创建一个同步方法。asynca++;

答:

3赞 StriplingWarrior 6/14/2023 #1

对于可以在合理时间内完成的“简单”同步方法,通常最好只以内联方式调用它们。让他们返回任务没有任何好处。但我怀疑他们是否真的应该.通常,如果要进行计算,函数应该是函数式的:将其输入作为参数,并将其输出作为返回值,而不是产生副作用。void

对于更耗时的同步方法,我仍然会让方法签名本身保持同步,但如果您需要快速响应用户(例如,您在 UI 线程上),您可以使用它们在单独的线程上运行这些方法。Task.Run()

我还建议仔细检查耗时的同步方法是否真的是同步的。例如,有一些方法可以异步执行 ed Process。现在,大多数 I/O 绑定操作都有一个版本,即使旧代码可能正在调用这些方法的同步版本。awaitStartAsync

但是,作为一般规则,任何异步都应该在某种程度上进行编辑:真正的即发即弃是危险的,尤其是当可能发生错误时。如果您不需要 UI 或请求者等待该任务完成,您可以将其移动到某种后台队列中,以确保等待所有任务并记录抛出的任何错误。await

评论

1赞 joaocarlosib 6/15/2023
谢谢你的回答。只是回答一下,我认为我们的 I/O 数据库操作还没有使用异步版本的唯一原因是因为它们是在遗留类型库中实现的,该库的接口仍在旧框架中使用,并且具有大量的内部验证。可以肯定的是,有些同事正在分离它的某些部分并使用更新的技术。