同步使用HttpClient的“正确方法”是什么?

问题描述 投票:0回答:5

我在“正确的方式”周围使用了引号,因为我已经很清楚使用异步 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 调用,因此我担心死锁。

那么处理这个问题的“正确方法”是什么?

c# asynchronous async-await
5个回答
131
投票

尝试以下操作:

var task = Task.Run(() => myHttpClient.GetAsync(someUrl)); 
task.Wait();
var response = task.Result;

仅当您无法使用

async
方法时才使用它。

正如 MSDN 博客中提到的,该方法完全无死锁: ASP.Net – 不要在主上下文中使用 Task .Result


73
投票

对于现在遇到此问题的任何人,.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();
}

此代码只是一个简化的示例,尚未准备好用于生产。


1
投票

您还可以查看使用 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


0
投票

与此同时,.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 包提供。


-1
投票

RestSharp 有一个 AsyncHelper,允许对异步方法进行同步调用(RestSharp 又从 Rebus 借用了该类)。

我过去曾使用过该类(我实际上只是复制并粘贴了它)来对异步方法进行同步调用,它的工作方式就像魅力一样。如果您想知道其工作原理和原因,Stephen Toub 的 Blog-Post 解释了 SynchronizationContextConfigureAwait(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;
        }
    }
© www.soinside.com 2019 - 2024. All rights reserved.