将async await集成到同步方法中

3

我已经苦恼了两天,但是我很生气,因为我似乎无法理解。

我在一个webapi上下文中。在此请求期间,我需要向我们的另一个系统发送一些数据,由于进行了大量计算和多个数据库保存等等,该系统返回速度很慢。无论操作是否成功,我都需要记录其结果。但是我不想等待它完成。

阅读到应该从头到尾使用async await。如果我决定这样做,我将不得不转换许多方法,因为我已经深入了3或4个方法,我担心这将分支更多。

我的选择是什么?如果我一直异步等待,那么我在更高层的堆栈中的方法(例如WebApi控制器)该怎么办?

这是我的代码,我已经尽量简化了。现在我在PushResult()方法中使用Task.Result()。据我所知,这会阻塞异步?这段代码可以发送请求。但是TestLog始终是最后一个,因此不是异步的。

    //I'm in a public service and referenced twice
    private void MyEndProcess()
    {
        // other stuff

        _vendorPushService.PushResult(); // This could take a while and I have to wait for it!

        _logService.PostLog(LogType.TestLog, "Test");
    }

    //I'm referenced above and somewhere else in the code base
    public void PushResult()
    {   
        ExternalResultModel externalResultModel = _resultService.GetExternalResultModel();

        PushedResultModel pushedResult = new PushedResultModel();

        try
        {
            pushedResult = _vendorRequestService.PushResultAsync(externalResultModel).Result;
        }
        catch (Exception ex)
        {
            pushedResult.Success = false;
        }

        if (pushedResult.Success)
        {
            _logService.PostLog(LogType.SuccessLog, pushedResult.Message);
        }
        else
        {
            _logService.PostLog(LogType.FailedLog, pushedResult.Message);
        }
    }

    public async Task<PushedResultModel> PushResultAsync(ExternalResultModel externalResultModel)
    {
        // setup the requestMessage
        HttpResponseMessage responseMessage = await _httpRequestService
            .SendRequest(requestMessage)
            .ConfigureAwait(false);

        return new PushedResultModel
        {
            Success = responseMessage.IsSuccessStatusCode,
            Message = await responseMessage.Content.ReadAsStringAsync()
        };
    }

    public class HttpRequestService : IHttpRequestService
    {
        private readonly HttpClient _httpClient; 

        public HttpRequestService(IHttpClientAccessor httpClientAccessor)
        {
            _httpClient = httpClientAccessor.HttpClient;
        }

        public async Task<HttpResponseMessage> SendRequest(HttpRequestMessage requestMessage)
        {
            HttpResponseMessage httpResponseMessage = await _httpClient.SendAsync(requestMessage).ConfigureAwait(false);

            return httpResponseMessage;
        }
    }

3
如果您不想完全使用异步方式,那么就让所有内容保持同步,并且不要添加任何异步代码。如果在不将整个调用堆栈变成异步的情况下添加一些异步代码,只会引起问题。 - Servy
2
如果你不想等待它完成,那么这基本上是一个“发射并忘记”的场景。在Web API的情况下,这有它自己的问题。我建议在这里看一下:http://blog.stephencleary.com/2014/06/fire-and-forget-on-asp-net.html。就回答你实际的问题而言,你可能想看一下`ContinueWith`,但请先阅读Stephen的文章。 - bornfromanegg
3
如果您需要启动后台操作并在返回响应给客户端之前不等待其完成 - async await 并不能够帮助太多。 - Evk
@Evk,你能详细说明一下吗?无论成功或失败,我都需要记录结果。但我不希望用户必须等待它完成。 - Jack Pettinger
1
然后阅读上面关于ASP.NET中“fire and forget”作业的链接。 - Evk
显示剩余2条评论
2个回答

0
你应该从顶部到底部实现async await。

如果我在下面全部使用async await,那么像WebApi控制器这样的堆栈中更高层的方法该怎么办?

只需像这样使您的控制器操作异步:

[RoutePrefix("api")]
public class PresidentsController : ApiController
{
    [Route("presidents")]
    public async Task<IHttpActionResult> GetPresidents()
    {
        await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false);
        return Ok();
    }
}

这是实现异步方法最简单的方式。即使要将所有内容更改为异步会增加一些工作量,但未来会有好处,因为您将避免许多与异步代码相关的问题。

如果你确实必须在同步方法中使用异步方法,请在一个地方将其阻塞,就像这样:

public void MySyncMethod()
    {
        try
        {
            this.MyAsyncMethod().Wait();

        }
        catch (Exception exception)
        {
            //omited
        }
    }
private async Task MyAsyncMethod()
{
     await AsyncLogic().ConfigureAwait(false);
}

但我不建议这样做。你应该一路使用异步等待到控制器动作。


2
除非您使用 ConfigureAwait(false),否则此示例在ASP.NET中将会死锁,即 await AsyncLogic().ConfigureAwait(false)。请参阅 https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html。 - bornfromanegg
你说得对,我忘了那个,谢谢。有趣的是它在本地主机上运行正常。示例已被编辑。 - garret

-1
在您的评论中,您说您想在后台处理任务,而不是让客户端调用您的API等待。为了做到这一点,您实际上不需要使用async/await。
尝试这个:
private void MyEndProcess()
{
    // other stuff

    Task.Run(_vendorPushService.PushResult()).ConfigureAwait(false); //fire and forget

    _logService.PostLog(LogType.TestLog, "Test");
}

Task.Run会启动任务,而ConfigureAwait(false)告诉它不需要在当前上下文中恢复(这意味着上下文可以在任务完成之前关闭 - 即可以在等待任务完成之前发送响应)。

您将收到编译器警告,提示您未等待Task.Run,但这正是您想要的。

请记住,这样做时,在PushResult中将无法使用HttpContext.Current


当您实际上没有等待时,ConfigureAwait是无意义的。配置一个您从未执行的等待不会实现任何目标。 - Servy
你还在倡导使用本质上异步的方法,阻塞等待方法结果,然后使用 Task.Run 在线程池线程中启动该同步-over-异步包装器。这样做非常混乱且低效。 - Servy
它确实会阻塞线程(直到任务完成),但不会阻塞请求,这正是OP所寻找的。我以前在Web API中使用过“fire-and-forget”任务。它有其危险性,但也有其适用场合。ConfigureAwait很重要,因为我发现如果没有它,响应将等待任务完成后才发送回来,这就不是“fire-and-forget”了。 - Gabriel Luci
哦,我甚至没有意识到“fire and forget”在这里完全不合适。不,这不是“fire and forget”的适当用法。这恰恰是一个不适当的使用方式。当然,如果你确实想要“fire and forget”(你并不想),那么你不会想要在异步操作上同步阻塞,只为了在线程池线程中执行同步工作,最终忽略结果,甚至不关心它是否运行。最后,在这里ConfigureAwait并不重要。它什么也没做。只有在有await时才会起作用。 - Servy
@GabrielLuci 我发现 ConfigureAwait 的文档不够详细,但它并不像你想的那样。ConfigureAwait(false) 并不意味着“不要等待此任务”。它的意思是,“如果等待此任务,则不要尝试在捕获的同步上下文中恢复。” 此处我们谈论的“上下文”是同步上下文。它不会“关闭”,就像你所说的那样。也许你把它和 HttpContext 混淆了? - bornfromanegg

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