.NET Core 2.0代码线程安全性咨询及异步方法复用问题
关于.NET Core中GetHtmlPageAsync线程安全与批量域名处理的问题解答
先来逐个拆解你的问题,同时梳理代码里的潜在风险:
1. DomainBody对象的线程安全性
DomainBody是一个无状态的普通POCO类,所有字段都是实例级别的,没有静态共享成员。每次调用GetHtmlPageAsync都会创建一个全新的DomainBody实例,每个线程操作的都是完全独立的对象,不存在多线程读写同一个实例的场景。所以单个DomainBody实例本身是线程安全的。
不过要注意:你把这些对象加入_domainList时,如果_domainList是普通的List<Domain>,那这个集合本身不是线程安全的!多个线程同时调用Add方法会破坏集合内部结构,甚至抛出异常。
2. GetHtmlPageAsync的线程安全性与批量处理的问题
先看GetHtmlPageAsync本身的线程安全情况
- 方法内的所有变量(比如
proxy、user_agent、body等)都是局部变量,每个异步调用都会生成独立的副本,不存在共享状态,这部分是安全的。 - 但有一个隐藏的线程安全坑:
new Random().Next().ToString()。Random类的Next()方法不是线程安全的,当多个线程同时调用时,会因为内部共享的种子状态导致生成重复的随机数,进而导致多个请求使用相同的sessionId,可能引发代理服务的异常。
再看批量处理逻辑ProcessDomainAsync的问题
这里有几个严重的错误,会导致逻辑失效:
- Parallel.ForEach不支持异步委托:你传入的
async (currentDomain) => {}是async void委托,Parallel.ForEach无法感知异步操作的完成,主线程会在所有异步任务结束前就返回domainCount,导致计数完全不准确,还无法捕获异步异常。 - 非线程安全的集合和计数器:
_domainList如果是普通List<Domain>,多线程Add会引发并发冲突;domainCount是普通int,多线程++操作不是原子操作,会导致计数错误。
修复建议
针对GetHtmlPageAsync的Random线程安全问题
把Random改为线程安全的实现,比如使用ThreadLocal<Random>:
// 在DomainProcessing类中添加静态字段 private static readonly ThreadLocal<Random> _threadRandom = new ThreadLocal<Random>(() => new Random(Guid.NewGuid().GetHashCode()) ); // 在GetHtmlPageAsync方法内替换原Random调用 var sessionId = _threadRandom.Value.Next().ToString();
也可以用.NET Core 2.1+的RandomNumberGenerator生成随机数,彻底避免线程安全问题。
针对批量处理的异步并发问题
如果你的项目是.NET 6及以上,推荐使用原生支持异步的Parallel.ForEachAsync:
public static async Task<int> ProcessDomainAsync(List<string> domains) { var domainList = new ConcurrentBag<Domain>(); int domainCount = 0; await Parallel.ForEachAsync(domains, async (currentDomain, cancellationToken) => { var domainBody = await GetHtmlPageAsync(currentDomain); var createdOn = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Local); var updatedOn = createdOn; // 统一时间戳,避免线程间时间差异 // ... 其余字段赋值逻辑 ... domainList.Add(new Domain { Url = currentDomain, Language = language, Country = country, MachineName = Environment.MachineName, Message = message, Active = active, Stage = stage, CreatedOn = createdOn, UpdatedOn = updatedOn }); Interlocked.Increment(ref domainCount); }); // 如果需要转成普通List,可使用domainList.ToList() return domainCount; }
如果是.NET 5及以下,用Task.WhenAll结合线程安全集合:
public static async Task<int> ProcessDomainAsync(List<string> domains) { var domainList = new ConcurrentBag<Domain>(); var tasks = domains.Select(async currentDomain => { var domainBody = await GetHtmlPageAsync(currentDomain); // ... 字段赋值逻辑 ... domainList.Add(new Domain { /* 赋值 */ }); }); await Task.WhenAll(tasks); return domainList.Count; }
另外,建议把HttpWebRequest替换为HttpClient(.NET Core推荐的HTTP客户端),HttpClient本身线程安全,配合HttpClientFactory使用还能避免频繁创建连接的性能损耗。
内容的提问来源于stack exchange,提问作者s vinayagam
相关产品推荐
相关产品推荐

