我在“正确的方式”周围使用了引号,因为我已经很清楚使用异步 API 的正确方式是简单地让异步行为在整个调用链中传播。这不是这里的选择。
我正在处理一个非常大且复杂的系统,专门设计用于在循环中同步进行批处理。
我突然使用 HttpClient 的原因是因为之前批处理的所有数据都是从 SQL 数据库收集的,现在我们添加了一个 Web API 调用。
是的,我们在同步执行循环中调用 Web API。我知道。将整个事情重写为异步并不是一种选择。这实际上就是我们想做的。 (我们正在尽可能减少 API 调用的数量)
我实际上确实尝试将异步行为传播到调用链上,但后来我发现自己有 50 个文件深度更改,仍然有数百个编译器错误需要解决,并且失去了所有希望。我失败了。
那么,回到问题,鉴于微软建议不要使用 WebRequest 进行新开发,而是使用仅提供异步 API 的 HttpClient,我该怎么办?
这是我正在做的一些伪代码...
foreach (var thingToProcess in thingsToProcess)
{
thingToProcess.ProcessStuff(); // This makes an API call
}
如何实现 ProcessStuff()?
我的第一个实现看起来像这样
public void ProcessStuff()
{
var apiResponse = myHttpClient // this is an instance of HttpClient
.GetAsync(someUrl)
.Result;
// do some stuff with the apiResponse
}
但是,我被告知,由于同步上下文,以这种方式调用 .Result 可能会导致从 ASP.NET 之类的东西调用时出现死锁。
你猜怎么着,这个批处理过程将从 ASP.NET 控制器启动。是的,我再次知道,这很愚蠢。当它从 ASP.NET 运行时,它只是“批处理”一项而不是整个批次,但我离题了,它仍然从 ASP.NET 调用,因此我担心死锁。
那么处理这个问题的“正确方法”是什么?
尝试以下操作:
var task = Task.Run(() => myHttpClient.GetAsync(someUrl));
task.Wait();
var response = task.Result;
仅当您无法使用
async
方法时才使用它。
正如 MSDN 博客中提到的,该方法完全无死锁: ASP.Net – 不要在主上下文中使用 Task .Result。
对于现在遇到此问题的任何人,.NET 5.0 已向
Send
添加了同步 HttpClient
方法。 https://github.com/dotnet/runtime/pull/34948
因此您可以使用它来代替
SendAsync
。例如
public string GetValue()
{
var client = new HttpClient();
var webRequest = new HttpRequestMessage(HttpMethod.Post, "http://your-api.com")
{
Content = new StringContent("{ 'some': 'value' }", Encoding.UTF8, "application/json")
};
var response = client.Send(webRequest);
using var reader = new StreamReader(response.Content.ReadAsStream());
return reader.ReadToEnd();
}
此代码只是一个简化的示例,尚未准备好用于生产。
您还可以查看使用 Nito.AsyncEx,它是一个 nuget 包。我听说过使用 Task.Run() 的问题,这解决了这个问题。这是 api 文档的链接: http://dotnetapis.com/pkg/Nito.AsyncEx/4.0.1/net45/doc/Nito.AsyncEx.AsyncContext
这是在控制台应用程序中使用异步方法的示例: https://blog.stephencleary.com/2012/02/async-console-programs.html
与此同时,.NET 6.0 已在
HttpClient
上获得同步支持(请参阅有关该主题的 GitHub 线程)。
但是,旧 .NET Framework 的用户被排除在外。因此,我实现了一个库,它通过实现自定义
HttpClient
并执行一些技巧来使 HttpClientHandler
调用(大部分)同步执行,从而为旧版 HttpContent
添加同步支持。在许多情况下,这应该可以防止死锁和线程池饥饿问题 - 您的情况可能会有所不同。
// Initialize the HttpClient with the custom handler
var client = new HttpClient(new HttpClientSyncHandler());
// Invoke sync HTTP call
using var request = new HttpRequestMessage(HttpMethod.Get, "https://1.1.1.1/");
using var response = client.Send(request);
// use the response here
// response.Content.ReadAsStream()
// response.Content.ReadAsByte[]()
// response.Content.ReadAsString()
来源:https://github.com/avonwyss/bsn.HttpClientSync - 它也可以作为 NuGet 包提供。
RestSharp 有一个 AsyncHelper,允许对异步方法进行同步调用(RestSharp 又从 Rebus 借用了该类)。
我过去曾使用过该类(我实际上只是复制并粘贴了它)来对异步方法进行同步调用,它的工作方式就像魅力一样。如果您想知道其工作原理和原因,Stephen Toub 的 Blog-Post 解释了 SynchronizationContext 和 ConfigureAwait(false) 的工作原理。
要将其与
HttpClient
一起使用,您可以这样做:
AsyncHelpers.RunSync(async () => await httpClient.SendAsync(...));
如果您打算制作一个同时支持 .NET-Framework 和 .NET-Core 的库/应用程序,您可以进一步优化它:
#if NETFRAMEWORK
//.NET-Framework does not support a sync Send
AsyncHelpers.RunSync(async () => await httpClient.SendAsync(...));
#elif NETCOREAPP
//.NET-Core does
httpClient.Send(...);
#else
//Default: Fallback to something that works on all targets.
AsyncHelpers.RunSync(async () => await httpClient.SendAsync(...));
#endif
为了完整起见,这里是
AsyncHelper
(再次强调,不是我的实现。我从 RestSharp 复制了它,并为了简洁而删除了注释)。
static class AsyncHelpers {
public static void RunSync(Func<Task> task) {
var currentContext = SynchronizationContext.Current;
var customContext = new CustomSynchronizationContext(task);
try {
SynchronizationContext.SetSynchronizationContext(customContext);
customContext.Run();
}
finally {
SynchronizationContext.SetSynchronizationContext(currentContext);
}
}
public static T RunSync<T>(Func<Task<T>> task) {
T result = default!;
RunSync(async () => { result = await task(); });
return result;
}
class CustomSynchronizationContext : SynchronizationContext {
readonly ConcurrentQueue<Tuple<SendOrPostCallback, object?>> _items = new();
readonly AutoResetEvent _workItemsWaiting = new(false);
readonly Func<Task> _task;
ExceptionDispatchInfo? _caughtException;
bool _done;
public CustomSynchronizationContext(Func<Task> task) =>
_task = task ?? throw new ArgumentNullException(nameof(task), "Please remember to pass a Task to be executed");
public override void Post(SendOrPostCallback function, object? state) {
_items.Enqueue(Tuple.Create(function, state));
_workItemsWaiting.Set();
}
public void Run() {
async void PostCallback(object? _) {
try {
await _task().ConfigureAwait(false);
}
catch (Exception exception) {
_caughtException = ExceptionDispatchInfo.Capture(exception);
throw;
}
finally {
Post(_ => _done = true, null);
}
}
Post(PostCallback, null);
while (!_done) {
if (_items.TryDequeue(out var task)) {
task.Item1(task.Item2);
if (_caughtException == null) {
continue;
}
_caughtException.Throw();
}
else {
_workItemsWaiting.WaitOne();
}
}
}
public override void Send(SendOrPostCallback function, object? state) => throw new NotSupportedException("Cannot send to same thread");
public override SynchronizationContext CreateCopy() => this;
}
}