[英]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
,另一方面您也可以为此使用线程同步构造,例如使用Semaphore
, Semaphore
就像队列,它允许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);
}
您不应该使用Parallel
或Task.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 内核有几种方法可以在 .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.