在同步类库中使用HttpClient

Posted

技术标签:

【中文标题】在同步类库中使用HttpClient【英文标题】:Using HttpClient in the synchronous class library 【发布时间】:2018-01-28 18:15:22 【问题描述】:

我正在使用 .Net Standard 为第 3 方 API 编写一个类库包装器,稍后我计划在我的其他项目中使用这个包装器。在浏览网络时,我发现人们普遍担心应该使用 HttpClient 类来发出 HTTP 请求。

我知道我可以采取两种方法:

    一直使用 async/await 到客户端项目 使用 Task.Wait() 方法

到目前为止,我将采用第一种方法。但这两种方法似乎都有问题。第一个比同步方法更难重用,同步方法返回它们的类型而不是 Task 对象。我宁愿采用第二种方法,但是容易出现死锁。

我的单次请求代码(HttpClient用无参数构造函数初始化):

    protected async Task<JObject> Request(string url)
    
        Uri uri = BuildUrl(url);
        HttpResponseMessage response = await HttpClient.GetAsync(uri);
        if (response.IsSuccessStatusCode)
        
            string result = await response.Content.ReadAsStringAsync();
            return JObject.Parse(result);
        

        return new JObject();
    

对于多个请求:

    protected async Task<JObject[]> Request(IEnumerable<string> urls)
    
        var requests = urls.Select(Request);
        return await Task.WhenAll(requests);
    

以及在包装类中的用法:

    protected async Task<JObject> RequestGet(string id, bool byUrl)
    
        if (IsBulk && !byUrl)
            return await Request($"Url?id=id");

        if (byUrl)
            return await Request(Url + id);

        return await Request(Url);
    

如何修改代码(第一个和第二个 sn-p),使其不会在每个调用方方法(第三个 sn-p)中导致任何死锁和异步使用?

【问题讨论】:

“第一个方法的可重用性不如同步方法”...我强烈认为这不是真的。在某处你正在做一些 IO 操作,async/await 绝对是在不阻塞的情况下处理这个问题的最简单方法。否则,您将不得不编写“APM 样式”并手动构建您的状态机或阻塞 IO。 HttpClient 是一个异步 API,旨在异步使用。长话短说,你不应该一直使用它 async 并避免阻塞同步调用或冒险死锁。 @ScottPerham 正如我现在所看到的那样,这种方法将使每个依赖于此类库的系统完全异步他们将处理与我完全相同的问题我正在努力寻找解决方案。 是的,但是他们可以选择使用“一直异步”(如 McGuireV10 所说),将其包装在 Task.Run 中,调用 Wait(),...使用取决于用例。作为库作者,您需要保持一致并使用“已知”的异步概念,例如 Task 是一个很好的方法(当然在我看来!) @gosferano 这么说吧:如果你只使用异步并且你的库的用户使用 Wait() 并且他们遇到了死锁,那是他们的错。但是,如果您在库中使用 Wait() 并且出现死锁,那是您的错。 :) 【参考方案1】:

不支持同步使用 HttpClient 并且容易出现死锁,并且您无法做任何事情(包括使用 Task.Wait)来改变这一事实。你有两个选择:

    仅支持异步。

    改用旧的WebRequest APIs 并以这种方式支持同步调用。

我会选择选项 1。最新最好的 HTTP 库甚至不再支持同步 I/O 是有原因的。多核处理器和分布式架构的新浪潮引发了编程世界的范式转变,在等待 I/O(尤其是 HTTP 等运行时间较长的网络调用)时占用线程完全是对资源的浪费。当像 C# 这样的语言为开箱即用的异步编程提供了令人难以置信的支持时,几乎没有充分的理由再进行同步 HTTP。大多数开发人员都理解这一点,我认为您不应该担心仅通过异步来放弃用户。相反,鼓励他们加快步伐以正确的方式做事。

【讨论】:

我完全理解这一点。这就是我在请求多个 URL 时进行异步调用的原因。我担心的是,它还会使我的应用程序也完全异步,而不是直接与 HttpClient 交互,并且只能通过包装器方法进行交互。但也许这只是我的错误想法。 正确地做意味着如果你的包装方法进行异步调用,那么这些包装方法本身必须也是异步的。也就是说,返回一个 Task 或 Task,并遵循标准模式,它们的名称应该以 Async 结尾。例如,如果您有一个通过 ID 获取 Thing 的包装方法,那么该方法的签名应该是 public async Task&lt;T&gt; GetThingAsync(id)(或类似名称)。这是一个用户友好的库 - 有经验的开发人员会确切地知道如何调用它。【参考方案2】:

一直使用async 是正确的方法,您可以从Task 方法返回结果(换句话说——正如你自己的代码所示——说他们不这样做是不对的' t 返回任务以外的任何内容):

public async Task<string> GetString(int value)

    return value.ToString();

显然不需要async,它不需要await 任何东西,但重点是Task 可以返回任何东西,这要归功于泛型。您甚至可以使用花哨的新 C# 7 值元组:

public async Task<(string name, int age)> GetUserInfo(int userId)  ... 

【讨论】:

我已经评论了这个问题本身,但我也想在这里引用它,因为你提出了这个解决方案:你是说,让所有方法异步更优化并且减少用例比使用 Wait() 自己? 托德在您接受的答案中所说的与我所说的相同:async 是正确的做法。

以上是关于在同步类库中使用HttpClient的主要内容,如果未能解决你的问题,请参考以下文章

java并发编程10.构建自定义的同步工具

在类库中使用 API 接口,在 API 接口中使用类库? [关闭]

在 C# 类库中使用 IConfiguration

#if DEBUG 指令在编译的类库中

如何在类库中使用重定向(url)方法?

使用类库中的视图 [重复]