C#实现异步并发下载 并发数大于1时无实时输出问题求助
问题根因
- 同步阻塞调用导致线程池饥饿:
MakeJOB中调用GetAsync、ReadAsStringAsync时使用.Result强制同步等待,会占用线程池线程不释放,并发量提高后线程池来不及分配新线程,所有任务的输出就会卡到接近全部完成才集中打印。 - 异步逻辑被强制同步执行:
NewRequest接收的是同步委托Func<HttpClient, T>,把异步请求逻辑包装成同步代码放到Task.Run中执行,没有真正利用异步IO不阻塞线程的特性。 - 重复自增索引的潜在bug:
MainAsync的循环中pos变量每次循环自增2次,会导致GethandlerIndexed取到超出数组长度的索引,触发空引用异常。 - HttpClient误用:每次请求都新建
HttpClient实例,频繁创建销毁会导致系统端口资源被快速占用,请求延迟升高。
修复方案
- 改造
NewRequest方法支持异步委托,修改方法签名和内部执行逻辑:
// 修改委托类型为支持异步的Func<HttpClient, Task<T>> public async Task<T> NewRequest<T>(Func<HttpClient, Task<T>> Expression, int? MaxTimeout = 2000, string Id = null) { await Locker.WaitAsync(MaxTimeout ?? 2000, TokenSource.Token); bool persistent = true; if (Id is null) { persistent = false; Id = string.Empty; } try { HttpClient client = await CreateClient(Id, persistent, TokenSource.Token); // 直接await异步委托,不需要套Task.Run T result = await Expression(client); if (persistent is false) client?.Dispose(); return result; } finally { Locker.Release(); } }
- 重写
MakeJOB方法,替换所有.Result为await,消除同步阻塞:
private static async Task<bool> MakeJOB(int pos) { return await cli.NewRequest<bool>(async (HttpClient client)=> { try { HttpClientHandler handler = null; if (cli.handler != null) handler = cli.GethandlerIndexed(pos); var requestClient = new HttpClient(handler); cli.AssignDefaultHeaders(requestClient); using HttpResponseMessage response = await requestClient.GetAsync("https://api.my-ip.io/ip.txt"); string ip = await response.Content.ReadAsStringAsync(); Console.WriteLine(ip + " / " + Task.CurrentId); return true; } catch { return false; } }); }
- 修复索引自增错误,修改
MainAsync中的循环逻辑:
static async Task MainAsync(string[] args) { cli.SetConcurrentDownloads(concurrency); var t = new Task[concurrency]; for (int i = 0; i < t.Length; i++) t[i] = MakeJOB(i); await Task.WhenAll(t); }
- 优化HttpClient复用:提前根据代理handler数组初始化对应HttpClient实例存储到
Clients字典中,每次请求直接取出使用,不要每次新建实例。
内容的提问来源于stack exchange,提问作者spoti plus
相关产品推荐
相关产品推荐

