从同步代码调用异步方法的正确方法是什么,并在任务完成之前阻塞?

时间:2015-04-22 10:12:58

标签: c# asynchronous async-await task-parallel-library

考虑以下代码:

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()方法。当SaveChanges()成功时,我还有List<MailMessage>个需要发送的电子邮件。所以我的代码看起来像这样:

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()到我的SendAsync(email)方法,它通过SMTP异步发送电子邮件。

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

3 个答案:

答案 0 :(得分:2)

  

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

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

在处理异步代码时,公开async over syncsync over async是反模式。

如果方法不是自然异步,我只会暴露同步版本,并让调用者决定是否要用Task.Run自己包装它。

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

异步方法一直走&#34;,当你使用它们时,它们会缓慢地在调用堆栈中上下浮动。接受这个事实,你将在此过程中避免许多陷阱。

答案 1 :(得分:1)

有一个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);
    }
}

我在没有问题的情况下在几个项目中使用它。

答案 2 :(得分:0)

如何进行阻止等待。

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

确实如此。关于

  

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

假设您还有此异步方法返回Task<string>

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

您可以执行以下操作来执行阻塞等待并检索其结果:

var myString = MyStringGetterAsync().Result;

关于您的意图:

  

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

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

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

最好在界面中明确,并且只提供符合其签名的方法。

针对您的真实世界问题(问题编辑后):

  

我有一个包含UnitOfWorkSaveChanges()的{​​{1}}类   EF6 SaveChangesAsync()的{​​{1}}种方法。我也有   DbContext只需要在发送时才需要发送的电子邮件   List<MailMessage>成功。

和(来自评论)

  

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

SaveChanges()UnitOfWork方法的当前Save设计不正确,因为它违反了它所做的原子性承诺。

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

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

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

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

Save

并像这样使用它:

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);
}