[英]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.