【问题标题】:Using Tasks for a lot of HTTP requests使用任务处理大量 HTTP 请求
【发布时间】:2021-07-17 19:04:47
【问题描述】:

所以我在使用 Tasks 处理大量 HTTP 请求时遇到了一些困难。

我想做的是从 WMTS 创建一个大图像。对于那些不知道的人,WMTS 是一种 Web 地图切片服务。 所以基本上,您可以通过发送带有正确 tileRow 和 tileColumn 的请求来请求 256x256 的图像图块。所以在这种情况下,我试图构建包含数百甚至数千个这样的图像块的图像。

为此,我创建了一个应用程序:

  • 根据输入计算它需要请求哪些图块。
  • 创建一个列表,我可以使用该列表向 WMTS 发出正确的 HTTP 请求。
  • 将这些请求发送到服务器并检索图像。
  • 将图像拼接成一张大图像。这就是我们想要的结果。

正如您可能想象的那样,图块的数量呈指数增长。这并不真正影响 CPU 工作,但主要是 I/O 绑定工作。 因此,与其等待每个请求返回,而是在发送下一个请求之前,我认为为此使用任务是个好主意。创建将处理每个单独请求的任务,当所有任务完成后,构建大图像。

所以这是我已经知道要请求哪些图块的方法。在这里,我想递归地发送带有任务的请求,直到所有数据都完成(最终使用最大重试机制)。

    public Dictionary<Tuple<int, int>, Image> GetTilesParallel(List<Tuple<int, int>> tileMatrix, int retry = 0)
    {
        //The dictionary we will return
        Dictionary<Tuple<int, int>, Image> images = new Dictionary<Tuple<int, int>, Image>();

        //The dictionary that we will recursively request if tiles fail.
        List<Tuple<int, int>> failedTiles = new List<Tuple<int, int>>();

        //To track when tasks are finished
        List<Task> tasks = new List<Task>();

        foreach (var request in tileMatrix)
        {
            Tuple<int, int> imageTile = new Tuple<int, int>(request.Item1, request.Item2);

            var t = Task.Factory.StartNew(() => { return GetTileData(imageTile.Item1, imageTile.Item2); }, TaskCreationOptions.LongRunning).ContinueWith(tsk =>
            {
                if (tsk.Status == TaskStatus.RanToCompletion)
                {
                    var response = tsk.Result.Result.Content.ReadAsByteArrayAsync().Result;
                    images.Add(imageTile, Image.FromStream(new MemoryStream(response)));
                }
                else
                {
                    failedTiles.Add(imageTile);
                }
            });

            tasks.Add(t);
        }

        Task.WaitAll(tasks.ToArray());

        if (failedTiles.Count > 0)
        {
            Console.WriteLine($"Retrying {failedTiles.Count} requests");
            Thread.Sleep(500);
            Dictionary<Tuple<int, int>, Image> retriedImages = GetTilesParallel(failedTiles, retry++);

            foreach (KeyValuePair<Tuple<int, int>, Image> retriedImage in retriedImages)
            {
                images.Add(retriedImage.Key, retriedImage.Value);
            }
        }
        return images;
    }

这是实际执行 HTTP 请求的方法(我知道不是最佳的或干净的,但我首先尝试让某些东西工作)。

    private async Task<HttpResponseMessage> GetTileData(int tileColumn, int tileRow)
    {
        WMTSSettings settings = Service.Settings;

        Dictionary<string, string> requestParams = new Dictionary<string, string>();
        requestParams.Add("Request", "GetTile");
        requestParams.Add("Style", "Default");
        requestParams.Add("Service", "WMTS");
        requestParams.Add("Version", this.Service.Version);
        requestParams.Add("TileMatrixSet", settings.WMTSTileMatrixSet);
        requestParams.Add("TileMatrix", settings.WMTSTileMatrixSet + ":" + settings.WMTSTileMatrix);
        requestParams.Add("Format", settings.ImageFormat);
        requestParams.Add("Layer", settings.Layer);
        requestParams.Add("TileCol", tileColumn.ToString());
        requestParams.Add("TileRow", tileRow.ToString());

        string requestString = this.Service.BaseUri;

        for (int i = 0; i < requestParams.Count; i++)
        {
            if (i == 0)
            {
                requestString += "?";
            }

            requestString += requestParams.ElementAt(i).Key;
            requestString += "=";
            requestString += requestParams.ElementAt(i).Value;

            if (i != requestParams.Count - 1)
            {
                requestString += "&";
            }
        }

        CancellationTokenSource source = new CancellationTokenSource();
        CancellationToken token = source.Token;

        Task<HttpResponseMessage> response = HttppClient.GetAsync(requestString, token);

        return await response;
    }

我目前面临两个问题,对此我尝试了很多方法:

  • 在当前设置中,在我的ContinueWith 任务中,我遇到了一些奇怪的错误,告诉我“对象引用未设置为对象的实例”。即使ContinueWith任务中的response变量和imageTile变量不为空?
  • 另一个问题是我仍然收到TaskCancellationExceptions。但如果我是正确的,这些异常应该被 Continuation 任务捕获吗?

有人可以为我指出这个问题的正确方向吗?还是任务甚至是要走的路?

【问题讨论】:

  • 阅读类似stackoverflow.com/questions/66635217/… 的内容?
  • 您有时会奇怪地使用await(正确)和.Result.WaitAll(不正确,阻塞。为什么会这样?
  • 你最好用await Task.WhenAll(.....)

标签: c# .net task-parallel-library


【解决方案1】:

是的,任务是要走的路,但不是,ContinueWithis not the way to go。这种方法主要是前 async-await 时代的遗物,现在很少有用了。 Task.Factory.StartNew 也是如此:引入Task.Run 方法后,您很少需要使用此方法。

创建下载磁贴数据所需的任务的一种便捷方法是使用 LINQ Select 运算符。你可以这样使用它:

async Task<Dictionary<(int, int), Image>> GetAllTileDataAsync(List<(int, int)> tiles)
{
    Task<(int, int, Image)>[] tasks = tiles.Select(async tile =>
    {
        (int tileColumn, int tileRow) = tile;
        int retry = 0;
        while (true)
        {
            try
            {
                using HttpResponseMessage response = await GetTileDataAsync(
                    tileColumn, tileRow);
                response.EnsureSuccessStatusCode();
                byte[] bytes = await response.Content.ReadAsByteArrayAsync();
                Image image = Image.FromStream(new MemoryStream(bytes));
                return (tileColumn, tileRow, image);
            }
            catch
            {
                if (retry >= 3) throw;
            }
            await Task.Delay(1000);
            retry++;
        }
    }).ToArray();
    (int, int, Image)[] results = await Task.WhenAll(tasks);
    return results.ToDictionary(e => (e.Item1, e.Item2), e => e.Item3);
}

每个图块都投影到Task&lt;(int, int, Image)&gt;。任务的结果包含有关图块的所有初始信息和获取的信息。这样就无需依赖危险的副作用来构建最终的Dictionary

请注意上述代码中缺少任何Task.Factory.StartNew.ContinueWith.Result.Wait()Task.WaitAll。在现代启用异步的应用程序中,所有这些方法都是危险信号。一切都是通过 async/await 组合发生的。 No threads 已创建,没有线程 are blocked,您的应用程序达到了最大的可伸缩性和响应能力。

【讨论】:

  • 关于TaskCanceledExceptions,这可能是相关的:HttpClient throws TaskCanceledException on timeout,或者这个:HttpClient - A task was cancelled?
  • 你我删除.ToArray()WhenAll 接受 IEnumerable 并立即对其进行评估。 HttpResponseMessage 也是 IDisposable
  • @aepot 是的,确实如此。为了简单起见,我添加了它(IEnumerable&lt;Task&lt;(int, int, Image)&gt;&gt; 类型看起来令人生畏),并且因为它可以防止误用。很容易意外地枚举IEnumerable 两次,然后因为所有任务都执行了两次而感到沮丧。实现IEnumerable 可以防止这种情况发生。
  • 也可以Image.FromStream(await response.Content.ReadAsStreamAsync()),或者这也是个把戏?
  • @aepot 我添加了一个using 声明来处理HttpResponseMessage。如果 GetTileDataAsync 返回 Task&lt;Image&gt; 而不是 Task&lt;HttpResponseMessage&gt; 可能会更好。
猜你喜欢
  • 2021-02-11
  • 2015-03-27
  • 1970-01-01
  • 2016-12-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多