【问题标题】:How to make concurrent API calls in C#?如何在 C# 中进行并发 API 调用?
【发布时间】:2019-09-27 15:11:28
【问题描述】:

我需要从抵押 API 中提取自定义字段。 问题是总共有 11000 条记录,每个 API 请求需要 1 秒。我想找到一种异步并行发送请求的方法,以提高效率。

我尝试遍历所有请求,然后使用Task.WaitAll() 等待响应返回。我只收到两个响应,然后应用程序无限期地等待。

我首先为HttpClient设置了一个静态类

 public static class ApiHelper
    {
        public static HttpClient ApiClient { get; set; }

        public static void InitializeClient()
        {
            ApiClient = new HttpClient();
            ApiClient.DefaultRequestHeaders.Add("ContentType", "application/json");
        }
    }

我收集我的抵押 ID 列表并循环通过 API Post Calls

        static public DataTable GetCustomFields(DataTable dt, List<string> cf, string auth)
        {

                //set auth header
                ApiHelper.ApiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", auth);

                //format body
                string jsonBody = JArray.FromObject(cf).ToString();
                var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");



                var responses = new List<Task<string>>();


                foreach (DataRow dr in dt.Rows)
                {

                    string guid = dr["GUID"].ToString().Replace("{", "").Replace("}", ""); //remove {} from string

                    responses.Add(GetData(guid, content));

                }

                Task.WaitAll(responses.ToArray());
                //some code here to process through the responses and return a datatable

                return updatedDT;

        }

每个 API 调用都需要 URL 中的抵押 ID (GUID)

  async static Task<string> GetData(string guid, StringContent json)
        {

            string url = "https://api.elliemae.com/encompass/v1/loans/" + guid + "/fieldReader";
            Console.WriteLine("{0} has started .....", guid);
            using (HttpResponseMessage response = await ApiHelper.ApiClient.PostAsync(url, json))
            {
                if (response.IsSuccessStatusCode)
                {
                    Console.WriteLine("{0} has returned response....", guid);
                    return await response.Content.ReadAsStringAsync();
                }
                else
                {
                    Console.WriteLine(response.ReasonPhrase);
                    throw new Exception(response.ReasonPhrase);
                }

            }

        }

我现在只测试 10 条记录并发送所有 10 条请求。 但我只收到两回。

结果是here

您能否告诉我发送并发 API 调用的正确方法?

【问题讨论】:

  • 看看TPL DataFlow

标签: c# api httpclient


【解决方案1】:

所有GetData Task 都使用相同的HttpClient 单例实例。 HttpClient 不能同时服务多个调用。最佳实践是使用 HttpClient 的Pool 以确保没有任务同时访问同一个 HttpClient。

另外,小心在Task中抛出exception,它会在第一次抛出异常时停止WaitAll()

解决方案我已经在这里发布了整个项目:https://github.com/jonathanlarouche/stackoverflow_58137212
此解决方案使用 [3] 的 max sized 池发送 25 个请求;

基本上,ApiHelper 包含一个 HttpClient pool,使用通用类 ArrayPool&lt;T&gt;您可以使用任何其他 Pooling 库,我只是想发布一个独立的解决方案

建议的 ApiHelper 下面,这个类现在包含一个池和一个接收 ActionUse 方法,池中的一个项目将在操作期间“租用”,然后它将通过ArrayPool.Use 函数返回到池中。 Use 函数还接收 apiToken 以更改请求身份验证标头。

public static class ApiHelper
{
    public static int PoolSize { get => apiClientPool.Size; }

    private static ArrayPool<HttpClient> apiClientPool = new ArrayPool<HttpClient>(() => {
        var apiClient = new HttpClient();
        apiClient.DefaultRequestHeaders.Add("ContentType", "application/json");
        return apiClient;
    });

    public static Task Use(string apiToken, Func<HttpClient, Task> action)
    {
        return apiClientPool.Use(client => {
            client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiToken);
            return action(client);
        });
    }
}

GetData 函数。 Get Data 将收到 apiToken 并等待 ApiHelper.Use 函数。 StringContent() 对象的新实例需要在此函数中完成,因为它不能在不同的 Http Post 调用中重复使用。

async static Task<string> GetData(string apiToken, Guid guid, string jsonBody)
{

    string url = "https://api.elliemae.com/encompass/v1/loans/" + guid + "/fieldReader";
    Console.WriteLine("{0} has started .....", guid);
    string output = null;
    await ApiHelper.Use(apiToken, (client) => 
    {
        var json = new StringContent(jsonBody, Encoding.UTF8, "application/json");
        return client.PostAsync(url, json).ContinueWith(postTaskResult =>
        {

            return postTaskResult.Result.Content.ReadAsStringAsync().ContinueWith(s => {

                output = s.Result;
                return s;
            });
        });
    });
    Console.WriteLine("{0} has finished .....", guid);
    return output;
}

数组池

public class ArrayPool<T>
{
    public int Size { get => pool.Count(); }
    public int maxSize = 3;
    public int circulingObjectCount = 0;
    private Queue<T> pool = new Queue<T>();
    private Func<T> constructorFunc;

    public ArrayPool(Func<T> constructorFunc) {
        this.constructorFunc = constructorFunc;
    }

    public Task Use(Func<T, Task> action)
    {
        T item = GetNextItem(); //DeQueue the item
        var t = action(item);
        t.ContinueWith(task => pool.Enqueue(item)); //Requeue the item
        return t;
    }

    private T GetNextItem()
    {
        //Create new object if pool is empty and not reached maxSize
        if (pool.Count == 0 && circulingObjectCount < maxSize)
        {
            T item = constructorFunc();
            circulingObjectCount++;
            Console.WriteLine("Pool empty, adding new item");
            return item;
        }
        //Wait for Queue to have at least 1 item
        WaitForReturns();

        return pool.Dequeue();
    }

    private void WaitForReturns()
    {
        long timeouts = 60000;
        while (pool.Count == 0 && timeouts > 0) { timeouts--; System.Threading.Thread.Sleep(1); }
        if(timeouts == 0)
        {
            throw new Exception("Wait timed-out");
        }
    }
}

【讨论】:

  • 如果你使用.Net Core,他可以使用HttpClientFactory来代替处理池
  • 如果他使用 asp.net,我将更改我的答案以使用 HttpClient 池的简单实现
  • @billybob 将解决方案更改为使用具有简单实现的池,以防他不使用 .Net 核心。感谢您的 cmets :D
  • 如果我有时间,我会尝试使用 TPL Dataflow 发布答案以同时执行多个调用。
  • 这解决了问题。我非常感谢你在这方面所做的工作。你超越了。我确实需要对 ArrayPools 进行一些教育,因为我以前从未使用过这些,但在 99 秒内处理了 1000 个请求,而之前是 16 分钟
猜你喜欢
  • 1970-01-01
  • 2018-11-06
  • 2019-03-15
  • 1970-01-01
  • 2023-04-03
  • 1970-01-01
  • 1970-01-01
  • 2021-09-20
  • 2019-03-31
相关资源
最近更新 更多