簡體   English   中英

使用 Parallel.ForEach 循環時的連接問題

[英]Connection problems while using Parallel.ForEach Loop

我有一個foreach循環,它負責執行一組特定的語句。 其中一部分是將圖像從 URL 保存到 Azure 存儲。 我必須為大量數據執行此操作。 為了實現相同的目的,我將foreach循環轉換為Parallel.ForEach循環。

 Parallel.ForEach(listSkills, item =>
 {
     // some business logic
     var b = getImageFromUrl(item.Url);
     Stream ms = new MemoryStream(b);

     saveImage(ms);
     // more business logic
 });

 private static byte[] getByteArray(Stream input)
 {
   using (MemoryStream ms = new MemoryStream())
   {
     input.CopyTo(ms);
     return ms.ToArray();
   }
 }

 public static byte[] getImageFromUrl(string url)
 {
    HttpWebRequest request = null;
    HttpWebResponse response = null;
    byte[] b = null;
    request = (HttpWebRequest)WebRequest.Create(url);
    response = (HttpWebResponse)request.GetResponse();
    if (request.HaveResponse)
    {
      if (response.StatusCode == HttpStatusCode.OK)
       {
          Stream receiveStream = response.GetResponseStream();
          b = getByteArray(receiveStream);
       }
    }

    return b;
 }

 public static void saveImage(Stream fileContent)
 {
  fileContent.Seek(0, SeekOrigin.Begin);
  byte[] bytes = getByteArray(fileContent);
  var blob = null;
  blob.UploadFromByteArrayAsync(bytes, 0, bytes.Length).Wait();
 }

盡管在某些情況下我會收到以下錯誤並且無法保存圖像。

現有連接被遠程主機強行關閉。

還共享 StackTrace:

   at System.Net.Sockets.NetworkStream.Read(Span`1 buffer)
   at System.Net.Security.SslStream.<FillBufferAsync>d__183`1.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Net.Security.SslStream.<ReadAsyncInternal>d__181`1.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.Security.SslStream.Read(Byte[] buffer, Int32 offset, Int32 count)
   at System.IO.Stream.Read(Span`1 buffer)
   at System.Net.Http.HttpConnection.Read(Span`1 destination)
   at System.Net.Http.HttpConnection.ContentLengthReadStream.Read(Span`1 buffer)
   at System.Net.Http.HttpBaseStream.Read(Byte[] buffer, Int32 offset, Int32 count)
   at System.IO.Stream.CopyTo(Stream destination, Int32 bufferSize)
   at Utilities.getByteArray(Stream input) in D:\repos\SampleProj\Sample\Helpers\CH.cs:line 238
   at Utilities.getImageFromUrl(String url) in D:\repos\SampleProj\Sample\Helpers\CH.cs:line 178

我猜這可能是因為我沒有使用鎖? 我不確定是否在Parallel.ForEach循環中使用鎖。

根據stackoverflow 上的另一個問題,以下是An existing connection was forced by the remote host 的潛在原因。 :

  • 您正在向應用程序發送格式錯誤的數據(其中可能包括向 HTTP 服務器發送 HTTPS 請求)
  • 客戶端和服務器之間的網絡鏈接由於某種原因正在關閉
  • 您在第三方應用程序中觸發了導致其崩潰的錯誤
  • 第三方應用程序已耗盡系統資源

由於只有您的部分請求受到影響,我認為我們可以排除第一個請求。 當然,這可能是網絡問題,在那種情況下,這會不時發生,具體取決於您和服務器之間的網絡質量。

除非您從其他用戶那里發現 AzureStorage 錯誤的跡象,否則您的調用很可能同時消耗過多的遠程服務器資源(連接/數據)。 服務器和代理對它們可以同時處理的連接數量有限制(尤其是來自同一台客戶端機器)。

根據listSkills列表的大小,您的代碼可能會並行啟動大量請求(與線程池一樣多),可能會淹沒服務器。

您至少可以像這樣使用MaxDegreeOfParallelism限制並行任務啟動的數量:

Parallel.ForEach(listSkills,
    new ParallelOptions { MaxDegreeOfParallelism = 4 },
    item =>
    {
         // some business logic
         var b = getImageFromUrl(item.Url);
         Stream ms = new MemoryStream(b);
    
         saveImage(ms);
         // more business logic
    });

您可以像這樣控制並行度:

listSkills.AsParallel()
                .Select(item => {/*Your Logic*/ return item})
                .WithDegreeOfParallelism(10)
                .Select(item =>
                  {
                      getImageFromUrl(item.url);
                      saveImage(your_stream);
                      return item;
                  });

但是Parallel.ForEach不適用於IO ,因為它專為CPU-intensive任務而設計,如果您將它用於IO-bound操作,特別是發出 web 請求,您可能會在等待響應時浪費線程池線程阻塞。

您使用異步 web 請求方法,例如HttpWebRequest.GetResponseAsync ,另一方面您也可以為此使用線程同步構造,例如使用SemaphoreSemaphore就像隊列,它允許X線程通過,rest 應該等到一個繁忙的線程將完成它的工作。 首先使您的getStream方法像async一樣(這不是好的解決方案,但可以更好):

public static async Task getImageFromUrl(SemaphoreSlim semaphore, string url)
{
    try
    {
        HttpWebRequest request = null; 
        byte[] b = null;
        request = (HttpWebRequest)WebRequest.Create(url);
        using (var response = await request.GetResponseAsync().ConfigureAwait(false))
        {
            // your logic
        } 
    }
    catch (Exception ex)
    {
        // handle exp
    }
    finally
    {
        // release
        semaphore.Release();
    }
}

然后:

using (var semaphore = new SemaphoreSlim(10))
{
    foreach (var url in urls)
    {
        // await here until there is a room for this task
        await semaphore.WaitAsync();
        tasks.Add(getImageFromUrl(semaphore, url));
    }
    // await for the rest of tasks to complete
    await Task.WhenAll(tasks);
}

您不應該使用ParallelTask.Run ,而是可以使用async處理程序方法,例如:

public async Task handleResponse(Task<HttpResponseMessage> response)
{
    HttpResponseMessage response = await response;
    //Process your data
}

然后像這樣使用Task.WhenAll

Task[] requests = myList.Select(l => getImageFromUrl(l.Id))
                        .Select(r => handleResponse(r))
                        .ToArray(); 
await Task.WhenAll(requests);

最后,您的場景有多種解決方案,但請忘記Parallel.Foreach ,而是使用優化的解決方案。

這段代碼有幾個問題:

  • Parallel.ForEach用於數據並行,而不是 IO。代碼凍結所有等待 IO 完成的 CPU 內核
  • HttpWebRequest 是 .NET Core 中 HttpClient 的包裝器。 使用 HttpWebRequest 效率低下並且比需要的復雜得多。
  • HttpClient 可以發布檢索或發布 stream 內容,這意味着沒有理由在 memory 中加載 stream 內容。HttpClient 是線程安全的,也意味着可以重用。

有幾種方法可以在 .NET Core 中同時執行許多 IO 操作。

.NET 6

在 .NET、.NET 6 的當前長期支持版本中,這可以使用Parallel.ForEachAsync來完成。 Scott Hanselman展示了使用它撥打 API 電話是多么容易

您可以使用GetBytesAsync直接檢索數據:

record CopyRequest(Uri sourceUri,Uri blobUri);
...
var requests=new List<CopyRequest>();
//Load some source/target URLs

var client=new HttpClient();
await Parallel.ForEachAsync(requests,async req=>{
    var bytes=await client.GetBytesAsync(req.sourceUri);
    var blob=new CloudAppendBlob(req.targetUri);
    await blob.UploadFromByteArrayAsync(bytes, 0, bytes.Length);
});

更好的選擇是將數據檢索為 stream 並將其直接發送到 blob:

await Parallel.ForEachAsync(requests,async req=>{
    var response=await client.GetAsync(req.sourceUri, 
                       HttpCompletionOption.ResponseHeadersRead);
    using var sourceStream=await response.Content.ReadAsStreamAsync();
    var blob=new CloudAppendBlob(req.targetUri);
    await blob.UploadFromStreamAsync(sourceStream);
});

HttpCompletionOption.ResponseHeadersRead導致GetAsync在收到響應標頭后立即返回,而不緩沖任何響應數據。

.NET 3.1

在較舊的 .NET Core 版本中(將在幾個月內達到生命周期結束),您可以使用例如並行度大於 1 的 ActionBlock:

var options=new ExecuteDataflowBlockOptions{ MaxDegreeOfParallelism = 8};

var copyBlock=new ActionBlock<CopyRequest>(async req=>{
    var response=await client.GetAsync(req.sourceUri, 
                       HttpCompletionOption.ResponseHeadersRead);
    using var sourceStream=await response.Content.ReadAsStreamAsync();
    var blob=new CloudAppendBlob(req.targetUri);
    await blob.UploadFromStreamAsync(sourceStream);
}, options);

TPL 數據流庫中的塊類可用於構建類似於 shell 腳本管道的處理管道,每個塊將其 output 管道傳輸到下一個塊。

暫無
暫無

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

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