繁体   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