从同步服务层边界调用异步方法

问题描述 投票:0回答:3

这可能已经在网络上的某个地方得到了回答,但我看了很多例子,但我仍然无法弄清楚。

我有一个基于同步的服务层边界(例如“ProcessOrder”方法)。在此方法中,它执行各种功能,其中一些具有异步重载,并且在包装 I/O 绑定函数时使用它们是有意义的。

但是,我的困惑有两个。据我读到的:

  • 异步代码应该是“一直异步”
  • 异步标记的方法不应返回 void - 这是事件处理程序的边缘情况并且
    不处理冒泡异常

因此,使用如下示例代码(注意:_publisher 是具有异步发送方法的 Azure QueueClient 的包装器):

public void ProcessOrder()
{
    // other stuff...

    _publisher.PublishAsync(new ItemOrderedEvent()
    {
        MessageId = Guid.NewGuid(),
        IsDurable = true,
    }).Wait();
}

似乎不符合“一路异步”规则,并且由于此模式在高负载下线程池耗尽,因此网络上有关于死锁等的警告,但代码如下:

public async void ProcessOrder()
{
    // other stuff...

   await  _publisher.PublishAsync(new ItemOrderedEvent()
    {
        MessageId = Guid.NewGuid(),
        IsDurable = true,
    });
}

打破了“异步标记方法不应返回 void”规则,并且不会处理异常,因为根据我的理解,没有任务上下文可以将它们从异步方法中冒泡回来。注意:这是一个服务边界 - 调用代码可能是 UI,响应 MSMQ 消息、Web 服务调用等,因此我不希望服务调用的“实现”泄漏到更高层。在当前情况下,IMO,这仅是同步调用是有意义的。

发布者方法定义为:

public async Task PublishAsync(IMessageBusMessage message)
{
    // do azure stuff
    var _queue = QueueClient.CreateFromConnectionString(connectionString, "ItemOrdered");
    await _queue.SendAsync(message);
}

所以我的问题是......你到底如何在基于同步的架构中使用合法的 I/O 绑定异步方法?典型的答案似乎是“你不能”(正如这个从同步上下文调用异步方法所建议的那样),但是我一定错过了一些东西,因为想要卸载 I/O 绑定工作并让当前线程在不基于 UI 的上下文中执行其他一些工作。

c# asynchronous async-await c#-5.0
3个回答
3
投票

注意:这是一个服务边界 - 调用代码可能是 UI,响应 MSMQ 消息、Web 服务调用等,所以我不希望服务调用的“实现”泄漏到更高层。在当前情况下,IMO,这仅是同步调用是有意义的。

事实上,你搞反了。 :)

考虑这个方法声明:

public void ProcessOrder();

这绝对是同步的。如果它执行任何异步操作,它将被阻塞,因此它会表现得像同步一样。可以将其解释为异步的唯一方法是,如果它采用某种回调或与事件或类似的东西配对,在这种情况下,它无疑是异步的。

现在考虑这个方法声明:

public Task ProcessOrderAsync();

这很可能是异步的。但如果它返回一个已经完成的任务,它可能是同步的。 Await 有一个“快速路径”快捷方式,它用来处理这种情况,并且实际上将其视为同步。缓存是实际使用的一个例子;缓存命中是同步的。 因此,如果您想抽象实现的异步性,那么您应该使用 asynchronous 方法声明。

注意:Task返回方法是(可能)异步声明,而

async

是实现细节。

也就是说,
有多种方法可以将异步代码“包装”到同步 API 中
,但是每种方法都有缺点,并且没有一种方法适用于所有场景。最好让你的 API 说实话:如果任何实现可能是异步的,那么方法声明也应该是异步的。

想要卸载 I/O 密集型工作似乎是完全合理的 让当前线程在非 UI 的上下文中执行一些其他工作 基于。

1
投票
如果这是一个非 UI 线程,并且您确实有一些其他工作要做,

您的 IO 绑定操作正在挂起,那么没有什么可以阻止您在同步方法中执行此操作:

public void ProcessOrder() { // other stuff... // initiate the IO-bound operation var task = _publisher.PublishAsync(new ItemOrderedEvent() {}); { MessageId = Guid.NewGuid(), IsDurable = true, }); // do not call .Wait() here // do your work for (var i = i; i < 100; i++) DoWorkItem(i); // work is done, wait for the result of the IO-bound operation task.Wait(); } 如果您无法重构所有代码以使用“一路异步”理念(这可能很耗时,但几乎总是可能的),这可能是有意义的。


到底如何在基于同步的架构中使用合法的 I/O 绑定异步方法?

1
投票
鱼与熊掌不可兼得。你要么采用异步,然后必须“一直异步”,要么采用同步,然后阻塞。

你可以旋转另一个线程并在那里执行阻塞 IO,但这是不好的做法,但这会浪费一个线程来阻塞,这样你就可以同时做更多的工作。

public void ProcessOrder() { // other stuff... Task.Run(async () => await _publisher.PublishAsync(new ItemOrderedEvent() { MessageId = Guid.NewGuid(), IsDurable = true, }).ContinueWith((Task task) => //do continuation if you need);

}

但是将如此松散的线松开是非常糟糕的做法。

如果可能的话,我要么一直异步,要么使用阻塞同步调用(像您在原始帖子中所做的那样调用 Result 或 Wait)

public async Task ProcessOrder() { // other stuff... await _publisher.PublishAsync(new ItemOrderedEvent() { MessageId = Guid.NewGuid(), IsDurable = true, }); }

© www.soinside.com 2019 - 2024. All rights reserved.