合同式异步和同步代码

3

有很多问题询问是否要混合异步和同步代码。

大多数答案都说,暴露同步包装器用于异步方法是一个坏主意,而暴露异步包装器用于同步方法也是如此。

然而,没有一个答案涉及到您必须混合异步和同步代码的具体情况,以及如何避免由此引起的常见陷阱。

请参考以下示例:

class Program
{
    static void Main(string[] args)
    {
        IContract signatory = new SyncSignatory();
        signatory.FullfillContractAsync().Wait();
        signatory = new AsyncSignatory();
        signatory.FullfillContractAsync().Wait();
    }
}

using System.Threading.Tasks;

interface IContract
{
    Task FullfillContractAsync();
}

using System.Threading.Tasks;

class AsyncSignatory : IContract
{
    public async Task FullfillContractAsync()
    {
        await Task.Delay(5000);
    }
}

using System.Threading;
using System.Threading.Tasks;

class SyncSignatory : IContract
{
    public Task FullfillContractAsync()
    {
        Thread.Sleep(5000);
        return Task.FromResult<object>(null);
    }
}

或者:

using System.Threading;
using System.Threading.Tasks;

class SyncSignatory : IContract
{
    public Task FullfillContractAsync()
    {
        return Task.Run(() => Thread.Sleep(5000));
    }
}

在这个例子中,SyncSignatory和AsyncSignatory代表两个可以互换的类,因为它们执行相似的功能,但是以不同的方式执行这些功能-同步和异步。
在避免常见问题场景的情况下,如何混合合同同步和异步代码?
如果一个用户期望syncSig异步运行,但实际上它是同步的,那怎么办?
如果一个用户原本可以通过异步运行来优化syncSig,但是现在不再这样做,因为他们认为它已经异步运行了,那该怎么办?

@DanWilson 已修改。 - Francisco Aguilera
我并没有看到问题所在。消费者只需要知道该方法的参数、名称和返回类型即可,该方法内部执行的操作对调用者来说并不重要。 - Jonesopolis
我认为如果你的示例太泛泛而谈,没有具体的用例,你将无法得到答案。谁将调用这些方法?您是否希望这些方法在代码的热路径中运行? - Yuval Itzchakov
2个回答

3
这是混合使用异步和同步的正确方式吗?
大多数情况下是。如果您有一个通用接口,其中一些实现可以提供同步实现,但其他实现只能提供异步实现,则返回Task<T>是有意义的。一个简单的return Task.FromResult可能是合适的。但也有例外情况,请参见下文。
那么,如果用户期望syncSig运行异步,但实际上它运行同步呢?
接口的某些方面无法通过代码表达。
如果您的IContract要求在返回Task之前不要阻塞调用线程超过X毫秒(在这种确切形式下不是一个合理的硬性要求,但您可以得到基本的想法),而它仍然阻塞线程,则这是违反契约的,用户应该向实现syncSig的人报告它是一个bug。
如果您的IContract要求不要抛出同步异常,任何错误都要使用Task.FromException或等效方法报告,而您的syncSig仍然抛出同步异常,则这也是违反契约的。
除此之外,如果syncSig立即返回正确的结果,那么没有理由会影响任何用户。
那么,如果有一个用户可以通过异步运行它来优化syncSig,但现在不再这样做,因为他们认为它已经运行异步了呢?
如果syncSig同步运行不会引起任何问题,则无关紧要,它不需要被优化。
如果syncSig同步运行会引起问题,则基本的调试工具很快就会告诉开发人员syncSig正在引起问题,并且应该进行调查。

第一个场景如何避免比第二个更多的陷阱?(在SyncSignatory同步中运行同步代码与异步运行它)。 - Francisco Aguilera
@FranciscoAguilera 我在你的问题中没有看到任何链接,也无法从你的评论中猜出你的意思。 - user743382
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Francisco Aguilera
@FranciscoAguilera 噢... Thread.Sleep(5000); return Task.FromResult<object>(null); 阻塞了当前线程,return Task.Run(() => Thread.Sleep(5000)); 阻塞了一个后台线程。假设你不仅仅是在睡眠,而是在做一些无法加速的事情。如果你的调用者需要你快速返回一个任务,你需要使用 Task.Run。如果你的调用者期望在函数返回时获得已完成的任务(因为你承诺过他们执行将是同步的),你需要避免使用 Task.Run。这里没有简单的规则,它取决于你的调用者的期望。 - user743382
@FranciscoAguilera 我不会假装知道哪些陷阱更常见。我都看过它们,很容易相信任何一个更常见。我可以尝试帮助你提供所需的信息,以便你为你的场景自行确定答案,但是你现在提供的额外信息并不足以让我回答得比我已经做的更详细,抱歉。你应该比我更清楚你的用户。 - user743382
显示剩余3条评论

2
然而,这些回答都没有涉及到必须混合异步和同步代码的具体情况以及如何避免由此引起的常见问题。这是因为这样做是一个不好的主意。任何混合代码都应该是完全临时的,只存在于转换为异步API的过程中。尽管如此,我已经写了一篇关于这个主题的整篇文章。正如我在关于异步接口的博客文章中所描述的那样,async是一种实现细节。返回任务的方法可能会同步完成。但是,我建议避免阻塞实现;如果由于某种原因它们是不可避免的,那么我会仔细记录它们的存在。

或者,您可以使用Task.Run作为实现;我不建议这样做(原因在我的博客详细说明中),但如果您必须有一个阻塞实现,那么这是一个选项。

对于期望syncSig异步运行的用户,但实际上它是同步运行的怎么办?

调用线程被同步阻塞。这通常只对UI线程造成问题-请参见下面的内容。

如果有一位用户本来可以通过异步运行来优化syncSig,但由于他们认为它已经是异步运行了,所以不再这样做,该怎么办?

如果调用代码是ASP.NET,则应直接调用它并await。同步代码将同步执行,在该平台上是可取的。

如果调用代码是UI应用程序,则需要知道存在同步实现,并且可以在Task.Run中包装它进行调用。这样无论实现是同步还是异步,都避免了阻塞UI线程。


在你的最后一段中,你是不是在说使用UI时,将对一个你认为是异步的方法进行包装调用是个好主意(例如Task DoSomethingAsync()),因为该方法仍然可能被实现为同步运行(例如{ return Task.CompletedTask; })?你只在代码与第三方代码之间的边界处这样做,因为你不知道后者如何实现你的接口,即同步还是异步?如果是这样,那么这是否意味着你会在UI中将所有对第三方代码的等待调用都包装在外部的Task.Run中? - rory.ap
@rory.ap:不,我的意思是如果实现是同步的(或可能是同步的),UI应用程序应该使用Task.Run。绝大多数返回Task的方法都不属于这个类别。 - Stephen Cleary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接