簡體   English   中英

使用 Tasks 處理大量 HTTP 請求

[英]Using Tasks for a lot of HTTP requests

所以我在使用 Tasks 處理大量 HTTP 請求時遇到了一些困難。

我想要做的是從 WMTS 創建一個大圖像。 對於那些不知道的人,WMTS 是一種 Web Map Tile Service。 因此,基本上,您可以通過發送具有正確 tileRow 和 tileColumn 的請求來請求 256x256 的圖像圖塊。 所以在這種情況下,我試圖構建包含數百甚至數千個這些圖像塊的圖像。

為此,我創建了一個應用程序:

  • 根據輸入計算它需要請求哪些圖塊。
  • 創建一個列表,我可以用它來向 WMTS 發出正確的 HTTP 請求。
  • 將這些請求發送到服務器並檢索圖像。
  • 將圖像拼接成一張大圖像。 這就是我們想要的結果。

正如您想象的那樣,圖塊的數量呈指數增長。 這不會真正影響 CPU 工作,但主要是 I/O 綁定工作。 因此,與其等待每個請求返回,不如在發送下一個請求之前,我認為為此使用 Tasks 是個好主意。 創建將處理每個單獨請求的任務,並在完成所有任務后,構建大圖像。

所以這是我已經知道我要請求什么瓷磚的方法。 在這里,我想遞歸地發送帶有任務的請求,直到所有數據都完成(最終使用最大重試機制)。

    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 任務捕獲嗎?

有人可以為我指出這個問題的正確方向嗎? 或者任務甚至是要走的路?

是的,任務是要走的路,但不, ContinueWith 不是要走的路 這種方法大多是前 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<(int, int, Image)> 任務的結果包含有關 tile 的所有初始和獲取信息。 這消除了依賴危險的副作用來構建最終Dictionary

注意到不存在任何的Task.Factory.StartNew.ContinueWith.Result.Wait()Task.WaitAll在上面的代碼。 所有這些方法在現代啟用異步的應用程序中都是危險信號。 一切都由 async/await 組合發生。 沒有線程被創建,沒有線程被阻塞,您的應用程序達到了最大的可擴展性和響應能力。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM