`Parallel.For` 併發 `WebClient.DownloadFile`,開始正常,運行一段時間後大量失敗,無異常信息。
// 1. 靜態 HttpClient(單例模式)
private static readonly HttpClient _client = new HttpClient(new HttpClientHandler {
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
}) { Timeout = TimeSpan.FromMinutes(5) };
// 2. Polly 重試策略
private static readonly AsyncRetryPolicy _retryPolicy = Policy
.Handle()
.Or()
.WaitAndRetryAsync(3, i => TimeSpan.FromSeconds(Math.Pow(2, i)));
// 3. 斷點續傳核心邏輯
public async Task DownloadWithResumeAsync(string url, string destPath, CancellationToken ct) {
long existingLength = 0;
if (File.Exists(destPath)) existingLength = new FileInfo(destPath).Length;
using (var request = new HttpRequestMessage(HttpMethod.Get, url)) {
if (existingLength > 0)
request.Headers.Range = new RangeHeaderValue(existingLength, null);
using (var response = await _retryPolicy.ExecuteAsync(() => _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct))) {
response.EnsureSuccessStatusCode();
using (var fs = new FileStream(destPath, FileMode.Append, FileAccess.Write, FileShare.None, 8192, true))
using (var stream = await response.Content.ReadAsStreamAsync()) {
await stream.CopyToAsync(fs, 8192, ct);
}
}
}
}
// 4. 受控併發下載(SemaphoreSlim 限制並發數)
var semaphore = new SemaphoreSlim(Environment.ProcessorCount * 2);
var tasks = urls.Select(async url => {
await semaphore.WaitAsync();
try { await DownloadWithResumeAsync(url, destPath, ct); }
finally { semaphore.Release(); }
});
await Task.WhenAll(tasks);