HttpClient.GetAsync(…)在使用await/async时从不返回

编辑: 这个问题看起来可能是同样的问题,但没有响应…

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

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

这只发生在使用新的async/await语言功能和任务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();
}
}
231507 次浏览

您在滥用API。

情况是这样的:在ASP。NET中,一次只能有一个线程处理一个请求。如果需要,您可以执行一些并行处理(从线程池中借用额外的线程),但是只有一个线程具有请求上下文(额外的线程没有请求上下文)。

这是由ASP管理。净SynchronizationContext

默认情况下,当你await一个Task时,该方法在捕获的SynchronizationContext上恢复(或捕获的TaskScheduler,如果没有SynchronizationContext)。通常,这正是你想要的:异步控制器动作将await一些东西,当它恢复时,它将与请求上下文一起恢复。

所以,下面是test5失败的原因:

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

以下是其他方法有效的原因:

  • (test1test2,和test3): Continuations_GetSomeDataAsync调度对线程池的延续,调度ASP。NET请求上下文。这允许由Continuations_GetSomeDataAsync返回的Task完成,而不必重新进入请求上下文。
  • (test4test6):因为Task等待,所以ASP。NET请求线程没有阻塞。这允许AsyncAwait_GetSomeDataAsync使用ASP。当它准备继续时,NET请求上下文。

以下是最佳实践:

  1. 在你的“库”async方法中,尽可能使用ConfigureAwait(false)。在你的情况下,这将改变AsyncAwait_GetSomeDataAsyncvar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. 不要阻塞Tasks;它一直是async。换句话说,使用await而不是GetResult (Task.ResultTask.Wait也应该替换为await)。

这样,你得到了两个好处:continuation (AsyncAwait_GetSomeDataAsync方法的剩余部分)在基本线程池线程上运行,不需要进入ASP。NET请求上下文;控制器本身是async(它不会阻塞请求线程)。

更多信息:

更新2012-07-13:结合这个答案。

编辑:一般来说,尽量避免做下面的事情,除非是为了避免死锁而做的最后努力。阅读Stephen Cleary的第一条评论。

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

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

试一试:

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

或者如果你需要一个结果:

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

从源代码(经过编辑以匹配上面的示例):

AsyncOperation现在将在线程池上调用 将不会是一个SynchronizationContext,以及内部使用的延续

. AsyncOperation不会被强制返回到调用线程

对我来说,这看起来是一个有用的选项,因为我没有选择使它异步的所有方式(这是我更喜欢的)。

来源:

确保FooAsync方法中的await没有找到上下文 元帅回到。最简单的方法是调用 线程池中的异步工作,例如通过包装 Task中的调用。运行,例如< / p >

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

FooAsync现在将在线程池上被调用,在那里不会有 SynchronizationContext,以及在FooAsync内部使用的延续 不会被强制返回到调用Sync()的线程

这两个学派并不是真正的排斥。

这里是您必须使用的场景

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

或者类似的东西

   AsyncContext.Run(AsyncOperation);

我有一个MVC操作,是在数据库事务属性下。这个想法(可能)是在发生错误时回滚操作中所做的所有事情。这不允许上下文切换,否则事务回滚或提交本身就会失败。

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

这是唯一的选择。运行它作为一个正常的同步调用。

我只是说,各有千秋。

由于你使用的是.Result.Waitawait,这最终会在你的代码中导致死锁

你可以在预防死锁async方法中使用ConfigureAwait(false)

是这样的:

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

你可以在任何可能的地方使用ConfigureAwait(false)来不阻塞异步代码。

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

最后发现我已经忘记了awaitasync调用进一步调用堆栈。

感觉就像少了一个分号。

在我的情况下,'等待'从来没有完成,因为在执行请求时异常,例如服务器不响应等。用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;
}

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

            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;