从同步代码调用异步方法并阻塞直到任务完成的正确方法是啥? [复制]

Posted

技术标签:

【中文标题】从同步代码调用异步方法并阻塞直到任务完成的正确方法是啥? [复制]【英文标题】:What is the correct way to call an async method from synchronous code, and block until the task is complete? [duplicate]从同步代码调用异步方法并阻塞直到任务完成的正确方法是什么? [复制] 【发布时间】:2015-06-29 22:05:42 【问题描述】:

考虑这段代码:

public async Task DoStuffAsync() 
    await WhateverAsync(); // Stuff that might take a while


// Elsewhere in my project...
public async Task MyMethodAsync() 
    await DoStuffAsync();


public void MyMethod() 
    DoStuffAsync(); // <-- I want this to block until Whatever() is complete.

我想提供两种功能相同的方法:异步运行的异步方法和阻塞的非异步方法,这两种方法中的任何一种都可以根据我调用的代码是否被调用也是异步的。

我不想到处重复加载代码并为每个方法创建两个版本,浪费时间和精力并降低可维护性。

我猜我可以执行以下操作之一:

// Call it just like any other method... 
// the return type would be Task though which isn't right.
DoStuffAsync();

// Wrap it in a Task.Run(...) call, this seems ok, but 
// but is doesn't block, and Task.Run(...) itself returns an
// awaitable Task... back to square one.
Task.Run(() => DoStuffAsync());

// Looks like it might work, but what if I want to return a value?
DoStuffAsync().Wait();

这样做的正确方法是什么?

更新

感谢您的回答。一般的建议似乎只是提供一个 Async 方法并让消费者决定,而不是创建一个包装器方法。

但是,让我试着解释一下我的现实问题......

我有一个 UnitOfWork 类,它包装了 EF6 DbContext 的 SaveChanges() 和 SaveChangesAsync() 方法。我还有List&lt;MailMessage&gt; 的电子邮件, 需要在 SaveChanges() 成功时发送。所以我的代码如下所示:

private readonly IDbContext _ctx;
private readonly IEmailService _email;
private readonly List<MailMessage> _emailQueue;

// ....other stuff    

public void Save() 
    try 
        _ctx.SaveChanges();
        foreach (var email in _emailQueue) _email.SendAsync(email).Wait();

     catch 
        throw;
    


public async Task SaveAsync() 
    try 
        await _ctx.SaveChangesAsync();
        foreach (var email in _emailQueue) await _email.SendAsync(email);

     catch 
        throw;
    

所以您可以看到,因为 EF6 DbContext 提供异步和非异步版本,所以我也必须这样做...根据目前的答案,我已将 .Wait() 添加到我的 @987654330 @ 方法,通过 SMTP 异步发送电子邮件。

所以...一切都很好,除了死锁问题...我如何避免死锁?

【问题讨论】:

DoStuffAsync().Result; 如果代码本质上是异步的,只需提供异步方法并让消费者决定在哪里或如何阻止它。不要提供只做消费者可以自己做的事情的同步包装方法。 事实上,Stephen Toub 就这个主题写了一篇很棒的blog post。 @Damien_The_Unbeliever:好点...但我仍然需要知道如何去做,因为我也在编写消费者代码 :) 我认为“正确”的方法是使消费者也异步,慢慢冒泡,直到您从 UI 到达事件侦听器。我想它并没有回答你关于如何阻止同步代码的问题。 【参考方案1】:

这样做的正确方法是什么?

正确的方法是做你不想做的事,公开两种不同的方法,一种是完全同步的,另一种是完全异步的。

在处理异步代码时,暴露 async over sync 和 sync over async 是反模式。

如果方法不是自然异步的,我将只公开一个同步版本,并让调用者决定他是否想用Task.Run 自己包装它。

如果该方法自然是异步的,我将只公开异步方法。如果您同时拥有同步版本和异步版本,请分别公开它们。

异步方法“一路走来”,当您使用它们时,它们会慢慢地在调用堆栈中上下冒泡。接受这一事实,您将避免一路走来的许多陷阱。

【讨论】:

【参考方案2】:

有一个 Github 项目:AsyncBridge

项目主页中包含的示例:

public async Task<string> AsyncString()

    await Task.Delay(1000);
    return "TestAsync";


public void Test()

    string string1 = "";
    string string2 = "";
    using (var A = AsyncHelper.Wait)
    
        A.Run(AsyncString(), res => string1 = res);
        A.Run(AsyncString(), res => string2 = res);
    

我在几个项目中使用它没有问题。

【讨论】:

【参考方案3】:

如何进行阻塞等待。

// Looks like it might work, but what if I want to return a value?
DoStuffAsync().Wait();

确实如此。关于

如果我想返回一个值怎么办?

假设您也有这个返回 Task&lt;string&gt; 的异步方法:

public async Task<string> MyStringGetterAsync() 
    return await GetStringAsync();
  

您可以通过以下方式执行阻塞等待并检索其结果:

var myString = MyStringGetterAsync().Result;

关于您的意图:

我想提供两种功能相同的方法:异步 异步运行的方法,以及阻塞的非异步方法, 并且可以根据是否调用这两种方法中的任何一种 我调用的代码也是异步的。

创建一个只执行 MyWhatEverAsync.Wait()MyWhatEver() 重载不是推荐的模式,因为它可能会对调用该方法的客户端产生意想不到的副作用,例如解释here。调用您方法的同步版本的客户端可能不希望其线程因调用您的方法而被阻塞,并且无法自行决定这是否可以接受(例如,它无法指定ConfigureAwait)。正如 cmets 中的 @Default 所提到的,这可能导致 deadlocks。

对于相反的情况通常也是如此:即有一个 MyWhatEverAsync() 只做 return Task.Run(() =&gt; MyWhatEver()); 如果客户需要,他们可以自己做这件事并包括例如取消令牌,因此他们可以控制任务。

最好在您的界面中明确说明,并且只提供按照签名要求执行的方法。

针对您的实际问题(问题编辑后):

我有一个 UnitOfWork 类,它包装了 SaveChanges()SaveChangesAsync() EF6 DbContext 的方法。我也有一个 List&lt;MailMessage&gt; 的电子邮件只有在 SaveChanges() 成功。

并且(来自 cmets)

发送电子邮件是否成功并不重要。

您当前为SaveSaveAsync 方法设计的UnitOfWork 是不正确的,因为它违反了它所做的原子性承诺。

在“SaveChanges”成功,但发送其中一封电子邮件时发生异常的情况。调用Save 变体之一的客户端代码将观察到该异常,将(并且应该)假设保存更改失败,并且可以重试应用它们。

如果客户端将此操作包装在具有两阶段提交的事务上下文中,则“SaveChanges”将回滚,并且当客户端重试该操作时,它将尝试再次发送电子邮件,可能会导致数字的收件人多次收到电子邮件,尽管他们收到电子邮件的更改未成功提交(如果发送电子邮件反复失败,则可能永远不会成功)。

所以你需要改变一些东西。

如果你真的不关心邮件发送成功,我建议你添加以下方法。

private Task SendEmailsAndIgnoreErrors(IEmailService emailService, List<MailMessage> emailQueue) 
    return Task.WhenAll(emailQueue.Select(async (m) => 
        try 
            await emailService.SendAsync(m).ConfigureAwait(false);
         catch 
            // You may want to log the failure though,
            // or do something if they all/continuously fail.
        
    ));

并像这样使用它:

public async Task SaveAsync() 
    await _ctx.SaveChangesAsync();
    await SendEmailsAndIgnoreErrors(_email, _emailQueue);

我个人会省略同步变体,让客户决定如何等待任务,但如果你认为你真的需要它,你可以这样做:

public void Save() 
    _ctx.SaveChanges();
    SendEmailsAndIgnoreErrors(_email, _emailQueue).Wait().ConfigureAwait(false);

【讨论】:

虽然,that has the possibility of creating deadlocks if you are not careful @Default 同意,仍在忙于添加答案的第二部分,与您可能不想这样做的原因有关。 @Alex:感谢您的回答...我已经更新了我的问题,解释了我为什么要这样做...如何避免死锁?

以上是关于从同步代码调用异步方法并阻塞直到任务完成的正确方法是啥? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

同步一步阻塞非阻塞

防止 DbContext 被释放,直到异步任务完成

同步异步与阻塞非阻塞

如何阻塞直到异步作业完成

008. 阻塞&非阻塞同步&异步

十异步