为什么 Task.Run(() => something) 调用等待死锁的同步方法?

Why does Task.Run(() => something) that calls a sync method that awaits deadlock?

我试图找出我正在从使用 WebRequest/WebResponse 转换为 HttpClient 的 TPL 程序中发生死锁的位置。

我有一个循环可以创建 100 个任务并将它们放在一个数组中:

var putTasks = new Task[100];
for (var i = 0; i < 100; i++)
{
    putTasks[i] = Task.Run(() => client.Put("something"));
}
Task.WaitAll(putTasks);

代码挂在 Task.WaitAll 上,查看服务器端显示第一个请求已完成,但其余请求未完成。

关于这个的奇怪部分(是的,它应该一直是异步的,但现在不是,我试图在完全异步之前理解它为什么会死锁)是 client.Put 是一个同步方法,看起来像这样:

string Put(string thing) { return PutAsync(string thing).GetAwaiter().GetResult(); }

PutAsync 看起来像:

async Task<string> PutAsync(string thing)
{
    //httpClient built up here
    HttpContent content = new StringContent(thing);

    var response = await httpClient.PutAsync("http://myuri", content);
    var result = await response.Content.ReadAsStreamAsync();
    // Do some stuff
}

它挂在 PutAsync 内部的部分是 response = await httpClient.PutAsync。我在 UI 线程中看到了很多关于死锁的答案,但是 none 修复(例如更改捕获的上下文)帮助。

理想情况下,我只是让它一直异步并等待一切,但正如我所说,我试图弄清楚为什么会发生死锁以及发生死锁的位置,然后再将所有东西分开 - 另一个问题是这是别人用的库,接口是同步方法,不能直接改成async,更新一堆隐藏代码。

问题是调用同步 API 的所有 运行ning 任务填满了线程池,因此 HttpClient 必须执行的异步调用没有线程 运行上。

Then please post an answer enlightening me on how I should take an API that provides synchronous methods (e.g. a client.Put(string thing) method and hook it up to an async method behind the scenes.

You don't. Doing so doesn't provide any benefit. Only the person who provided the library should be wrapping calls they know for fact call some type of Completion Port。创建一个单独的线程(Task.Run())来等待一个资源简直是在浪费资源(也有例外)。

I can't just change it to async and update a bunch of hidden code.

你不应该。

The part it's hanging on inside PutAsync is the response = await httpClient.PutAsync

99% 的死锁是因为 callerMethodAsync() 没有正确完成,没有那个特定的代码,知道为什么是无法回答的。

这仅在您知道您正在使用完成端口时才有用

我无法演示 PutAsync,因为创建真正的异步方法的要求在 HttpClient 上不可用。相反,我将创建 on for SqlCommand for ExecuteNonQuery():

public static Task<int> ExecuteNonQueryAsync(this SqlCommand sqlCommand)
{
  var tcs = new TaskCompletionSource<int>();

  sqlCommand.BeginExecutedNonQuery(result =>
  {
    tcs.SetResult(sqlCommand.EndExecutedNonQuery());
  });

  return await tcs.Task;
}

当然这是非常糟糕的编码,没有 try/catch 等,但它不使用 Task.Run() 来启动另一个什么都不做的线程。

现在,如果您想使用异步任务正确执行并行任务调用,那么这样的事情更合适(并且不会浪费线程):

public async Task<IEnumerable<HttpResponseMessage>> GoCrazy()
{
  var putTasks = new List<Task<HttpResponseMessage>>();

  for (var i = 0; i < 100; i++)
  {
    var task = client.PutAsync("something");
    putTasks.Add(task);
  }

  // WhenAll not WaitAll
  var result = await Task.WhenAll(putTasks);

  return result;
}