繁体   English   中英

在Task.Factory.StartNew中封装异步方法

[英]Encapsulating async method in Task.Factory.StartNew

我对Task和async / await模式有很好的理解,但是最近有人修复了他所说的由以下原因造成的死锁:

public async Task<HttpResponseMessage> GetHttpResponse()
{
    using (var client = new HttpClient())
    {
        return await client.SendAsync(new HttpRequestMessage()).ConfigureAwait(false);
    }
}

他说他用某种Task.Factory.StartNew模式修复了它。

public HttpResponseMessage GetHttpResponse()
{
    using (var client = new HttpClient())
    {
        return Task.Factory.StartNew(() => client.SendAsync(new HttpRequestMessage()).Result).Result;
    }
}

首先,问题是我为什么要将return语句更改为HttpResponseMessage而不是Task<HttpResponseMessage>

我的第二个问题是,为什么这段代码会解决死锁错误。 从我的理解,即他所说的事实.Result迫使GetHttpResponse线程等待(并冻结),直到client.SendAsync完成。

任何人都可以尝试解释这个代码如何影响任何TaskSchedulerSynchronizationContext

谢谢您的帮助。

编辑:这是调用方法,为问题提供更多的上下文

public IWebRequestResult ExecuteQuery(ITwitterQuery twitterQuery, ITwitterClientHandler handler = null)
{
    HttpResponseMessage httpResponseMessage = null;

    try
    {
        httpResponseMessage = _httpClientWebHelper.GetHttpResponse(twitterQuery, handler).Result;

        var result = GetWebResultFromResponse(twitterQuery.QueryURL, httpResponseMessage);

        if (!result.IsSuccessStatusCode)
        {
            throw _exceptionHandler.TryLogFailedWebRequestResult(result);
        }

        var stream = result.ResultStream;

        if (stream != null)
        {
            var responseReader = new StreamReader(stream);
            result.Response = responseReader.ReadLine();
        }

        return result;
    }
// ...

用于修复死锁的修改代码使用Task APIs非常糟糕,我可以看到很多问题:

  1. 它已将Async call to Sync call转换Async call to Sync call ,由于在Task上使用Result,因此它是一个阻塞调用
  2. Task.Factory.StartNew ,是一个很大的不,检查StartNew是Stephen Cleary 危险

关于您的原始代码,以下是最可能的deadlock原因:

  • ConfigureAwait(false)不起作用的原因是您需要在所有Async calls中的完整调用堆栈中使用它,这就是它导致Deadlock的原因。 Point是client.SendAsync和完整的调用链需要有一个ConfigureAwait(false) ,以便忽略Synchronization context 只是在一个地方并没有帮助,并且不能保证不在您控制范围内的电话

可能解决方案

  • 在这里简单的删除应该工作,你甚至不需要Task.WhenAll ,因为没有多个tasks

      using (var client = new HttpClient()) { return await client.SendAsync(new HttpRequestMessage()); } 

另一个不太优选的选择是:

  • 使代码同步如下(现在整个链中不需要ConfigureAwait(false) ):

     public HttpResponseMessage GetHttpResponse() { using (var client = new HttpClient()) { return await client.SendAsync(new HttpRequestMessage()).ConfigureAwait(false).GetAwaiter().GetResult(); } } 

死锁不是由return await client.SendAsync(new HttpRequestMessage()).ConfigureAwait(false);引起的return await client.SendAsync(new HttpRequestMessage()).ConfigureAwait(false); 这是由于阻塞调用堆栈引起的。 因为它是非常不太可能的MS执行HttpClient.SendAsync()具有一定的阻挡代码内(这可能死锁),它必须是在呼叫者之一到public async Task<HttpResponseMessage> GetHttpResponse()使用.Wait().Result返回任务的.Result 你的同事所做的就是将阻塞调用移动到堆栈中,在那里它更加明显。 此外, 这个“修复”甚至不能解决传统的死锁,因为它使用相同的同步上下文(!)。 你可以在堆栈的其他地方推断出一些其他函数卸载没有asp.net同步上下文的新任务,并且在新的(可能是默认的)上下文中阻塞GetHttpResponse()执行,否则“修复”也会出现死锁!

因为在现实世界中,并不总是可以将生成遗留代码重构为异步,所以您应该使用自己的异步HttpClient接口,并确保实现使用.ConfigureAwait(false) ,因为所有基础结构库都应该如此。

@ mrinal-kamboj @ panagiotis-kanavos @shay

感谢大家的帮助。 如上所述,我已经开始阅读Async in C# 5.0 from Alex DaviesAsync in C# 5.0 from Alex Davies

我找到了什么

在第8章:哪个线程运行我的代码 ,他提到我们的情况,我想我在那里找到了一个有趣的解决方案:

您可以通过在启动异步代码之前移动到线程池来解决死锁问题,以便捕获的SynchronizationContext是线程池而不是UI线程。

var result = Task.Run(() => MethodAsync()).Result;

通过调用Task.Run他实际上强制异步调用SynchronizationContext成为ThreadPool的SynchronizationContext (这是有意义的)。 通过这样做,他还确保代码MethodAsync既不启动也不返回主线程。

我的解决方案

考虑到这一点,我改变了我的代码如下:

public HttpResponseMessage GetHttpResponse()
{
    using (var client = GetHttpClient())
    {
        return TaskEx.Run(() => client.SendAsync(new HttpRequestMessage())).Result;
    }
}

此代码似乎适用于Console,WPF,WinRT和ASP.NET。 我会进一步测试并更新这篇文章。

问题

  • 你认为这个新代码有意义吗?
  • 你认为它会阻止任何潜在的僵局吗?

注意

在本书中,我了解到.ConfigureAwait(false)仅阻止回调调用SynchronizationContext.Post()方法在调用者Thread上运行。 要确定应该运行回调的线程, SynchronizationContext检查与其关联的线程是否重要 如果是,那么它会选择另一个线程。

根据我的理解,这意味着回调可以在任何线程(UI-Thread或ThreadPool)上运行。 因此,它不保证UI-Thread上的不执行,但使其不太可能。

笔记2)

有趣的是,以下代码不起作用:

public async Task<HttpResponseMessage> GetHttpResponse()
{
    using (var client = GetHttpClient())
    {
        return await TaskEx.Run(() => client.SendAsync(new HttpRequestMessage()));

当我尝试使用这段代码时,我记得.Result可以在ThreadPool范围之外使用.Result 在某种程度上它对我有意义,但如果有人想对此发表评论,他将受到欢迎:)

我想加入ConfigureAwait到您的注(2)代码将做它的工作。 ASP.NET上下文一次只允许1个线程在其中运行。 线程池上下文允许一次运行多个线程。

await将代码执行返回给调用方法。 调用方法在ASP.net上下文和.Result块的调用。 client.SendAsync返回时,它将完成ASP.net上下文中的方法,但不能,因为上下文中已经有一个线程阻塞(对于.Result)。

如果您将ConfigureAwaitclient.SendAsync一起client.SendAsync ,它将能够在与ASP.Net上下文不同的上下文中完成该方法,并在调用方法中完成获取.Result的方法。

即.Result正在等待GetHttpResponse方法的结束,而GetHttpResponse方法的结束正在等待。 结果退出ASP.net上下文完成。

在你原来的例子中,当调用client.SendAsync等待client.SendAsync离开ASP.net上下文继续时,调用方法阻塞client.SendAsync 就像你说的那样, ConfigureAwait(false)只会影响await之后从返回执行的代码的上下文。 Client.SendAsync正等待进入ASP.NET上下文执行。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM