344

编辑: 这个问题看起来可能是同一个问题,但没有回应......

编辑:在测试用例 5 中,任务似乎卡在WaitingForActivation状态。

我在 .NET 4.5 中使用 System.Net.Http.HttpClient 遇到了一些奇怪的行为——“等待”调用 (eg) 的结果httpClient.GetAsync(...)将永远不会返回。

这仅在使用新的 async/await 语言功能和 Tasks API 时发生在某些情况下 - 仅使用延续时代码似乎总是有效。

这是一些重现问题的代码 - 将其放入 Visual Studio 11 中的新“MVC 4 WebApi 项目”中,以公开以下 GET 端点:

/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6

这里的每个端点都返回相同的数据(来自 stackoverflow.com 的响应标头),但/api/test5永远不会完成。

我是否在 HttpClient 类中遇到了错误,或者我是否以某种方式滥用了 API?

重现代码:

public class BaseApiController : ApiController
{
    /// <summary>
    /// Retrieves data using continuations
    /// </summary>
    protected Task<string> Continuations_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString());
    }

    /// <summary>
    /// Retrieves data using async/await
    /// </summary>
    protected async Task<string> AsyncAwait_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return result.Content.Headers.ToString();
    }
}

public class Test1Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await Continuations_GetSomeDataAsync();

        return data;
    }
}

public class Test2Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = Continuations_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test3Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return Continuations_GetSomeDataAsync();
    }
}

public class Test4Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await AsyncAwait_GetSomeDataAsync();

        return data;
    }
}

public class Test5Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = AsyncAwait_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test6Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return AsyncAwait_GetSomeDataAsync();
    }
}
4

7 回答 7

499

您在滥用 API。

情况如下:在 ASP.NET 中,一次只有一个线程可以处理一个请求。如有必要,您可以进行一些并行处理(从线程池中借用额外的线程),但只有一个线程具有请求上下文(其他线程没有请求上下文)。

这是由 ASP.NET 管理的SynchronizationContext

默认情况下,当您awaita时,该方法会在捕获的(或捕获的,如果没有)Task上恢复。通常,这正是您想要的:异步控制器操作会有所作为,当它恢复时,它会使用请求上下文恢复。SynchronizationContextTaskSchedulerSynchronizationContextawait

所以,这就是test5失败的原因:

  • Test5Controller.Get执行AsyncAwait_GetSomeDataAsync(在 ASP.NET 请求上下文中)。
  • AsyncAwait_GetSomeDataAsync执行HttpClient.GetAsync(在 ASP.NET 请求上下文中)。
  • HTTP 请求被发送出去,并HttpClient.GetAsync返回一个未完成的Task.
  • AsyncAwait_GetSomeDataAsync等待Task; 因为它不完整,所以AsyncAwait_GetSomeDataAsync返回一个 uncompleted Task
  • Test5Controller.Get 阻塞当前线程直到Task完成。
  • HTTP响应进来了,Task返回的HttpClient.GetAsync就完成了。
  • AsyncAwait_GetSomeDataAsync尝试在 ASP.NET 请求上下文中恢复。但是,该上下文中已经有一个线程:线程被阻塞在Test5Controller.Get.
  • 僵局。

这就是其他工作的原因:

  • ( test1, test2, 和test3):在 ASP.NET 请求上下文之外Continuations_GetSomeDataAsync将继续调度到线程池。这允许返回者完成,而无需重新输入请求上下文。TaskContinuations_GetSomeDataAsync
  • (test4test6):由于Taskawaited,所以 ASP.NET 请求线程没有被阻塞。这允许AsyncAwait_GetSomeDataAsync在准备好继续时使用 ASP.NET 请求上下文。

以下是最佳实践:

  1. 在您的“库”async方法中,ConfigureAwait(false)尽可能使用。在您的情况下,这将更AsyncAwait_GetSomeDataAsync改为var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. 不要在Tasks 上阻塞;它async一直在下降。换句话说,使用await代替GetResult(Task.Result并且Task.Wait也应该替换为await)。

这样,您将获得两个好处:延续(AsyncAwait_GetSomeDataAsync方法的其余部分)在不必进入 ASP.NET 请求上下文的基本线程池线程上运行;并且控制器本身是async(它不会阻塞请求线程)。

更多信息:

2012-07-13 更新:将此答案合并到博客文章中。

于 2012-04-27T13:20:20.143 回答
65

编辑:通常尽量避免执行以下操作,除非作为避免死锁的最后努力。阅读 Stephen Cleary 的第一条评论。

从这里快速修复。而不是写:

Task tsk = AsyncOperation();
tsk.Wait();

尝试:

Task.Run(() => AsyncOperation()).Wait();

或者,如果您需要结果:

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

从源(编辑以匹配上面的例子):

AsyncOperation 现在将在没有 SynchronizationContext 的 ThreadPool 上调用,并且在 AsyncOperation 内部使用的延续不会被强制返回到调用线程。

对我来说,这看起来像是一个可用的选项,因为我没有选择让它一直异步(我更喜欢)。

从来源:

确保 FooAsync 方法中的 await 找不到要封送回的上下文。最简单的方法是从 ThreadPool 调用异步工作,例如通过将调用包装在 Task.Run 中,例如

int Sync() { return Task.Run(() => Library.FooAsync()).Result; }

FooAsync 现在将在没有 SynchronizationContext 的 ThreadPool 上调用,并且 FooAsync 内部使用的延续不会被强制返回到调用 Sync() 的线程。

于 2013-11-01T13:23:52.080 回答
16

由于您正在使用.Resultor.Wait或,await这最终会导致您的代码出现死锁

您可以ConfigureAwait(false)防止死锁async的方法中使用

像这样:

var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead)
                             .ConfigureAwait(false);

您可以ConfigureAwait(false)尽可能使用 Don't Block Async Code 。

于 2018-01-20T15:12:52.873 回答
2

这两所学校并没有真正排除。

这是您只需使用的场景

   Task.Run(() => AsyncOperation()).Wait(); 

或类似的东西

   AsyncContext.Run(AsyncOperation);

我有一个位于数据库事务属性下的 MVC 操作。这个想法(可能)是在出现问题时回滚操作中所做的所有事情。这不允许上下文切换,否则事务回滚或提交将自行失败。

我需要的库是异步的,因为它预计会异步运行。

唯一的选择。将其作为正常的同步调用运行。

我只是对每个人说。

于 2017-02-08T08:19:04.237 回答
1

我将把它放在这里更多是为了完整性,而不是与 OP 直接相关。我花了将近一天的时间来调试一个HttpClient请求,想知道为什么我从来没有得到回应。

最后发现我忘记awaitasync调用堆栈的更下方的调用。

感觉就像少了一个分号一样好。

于 2019-05-21T19:05:27.137 回答
1

在我的情况下,“等待”从未完成,因为执行请求时出现异常,例如服务器没有响应等。用 try..catch 包围它以识别发生了什么,它也会优雅地完成你的“等待”。

public async Task<Stuff> GetStuff(string id)
{
    string path = $"/api/v2/stuff/{id}";
    try
    {
        HttpResponseMessage response = await client.GetAsync(path);
        if (response.StatusCode == HttpStatusCode.OK)
        {
            string json = await response.Content.ReadAsStringAsync();
            return JsonUtility.FromJson<Stuff>(json);
        }
        else
        {
            Debug.LogError($"Could not retrieve stuff {id}");
        }
    }
    catch (Exception exception)
    {
        Debug.LogError($"Exception when retrieving stuff {exception}");
    }
    return null;
}
于 2020-10-26T16:16:03.417 回答
1

我使用了很多等待,所以我没有得到响应,我转换为同步调用它开始工作

            using (var client = new HttpClient())
            using (var request = new HttpRequestMessage())
            {
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                request.Method = HttpMethod.Get;
                request.RequestUri = new Uri(URL);
                var response = client.GetAsync(URL).Result;
                response.EnsureSuccessStatusCode();
                string responseBody = response.Content.ReadAsStringAsync().Result;
               
于 2021-02-03T12:35:07.830 回答