技术问询:ActionBlock收集结果时需用BlockingCollection替代List吗?
results集合类型的选择分析 嘿,你的判断其实是不必要的——在这个特定的测试代码里,完全不需要把List<int>换成BlockingCollection<int>,原因如下:
ActionBlock默认串行执行,不会有线程冲突
ActionBlock<int>的默认配置是单线程处理消息(除非你特意修改ExecutionDataflowBlockOptions里的MaxDegreeOfParallelism参数)。看你的代码,即使启用了注释里的consumerOptions,你设置的BoundedCapacity = 1也不影响并发处理数,真正控制并发的是MaxDegreeOfParallelism(默认值为1)。这意味着results.Add(x)这个操作永远只会被一个线程调用,List<int>的线程不安全问题根本不会出现。等待逻辑确保集合修改完成后才读取
你的代码最后通过await Task.WhenAll(producers, consumer.Completion);等待所有生产者完成消息发送,同时也等待消费者处理完所有队列中的消息。这就保证了在执行断言逻辑时,results的所有添加操作已经全部完成,不存在异步修改和同步读取的冲突场景。
举个更清晰的例子,你的核心测试逻辑可以简化为这样(关键部分不变):
[TestMethod] public async Task ConsumerReceivesCorrectValues() { var results = new List<int>(); var queue = new BufferBlock<int>(new DataflowBlockOptions { BoundedCapacity = 5, }); // 默认单线程执行的消费者 var consumer = new ActionBlock<int>(x => results.Add(x)); queue.LinkTo(consumer, new DataflowLinkOptions { PropagateCompletion = true, }); var producers = ProduceAll(queue); // 等待所有生产、消费操作完成 await Task.WhenAll(producers, consumer.Completion); // 此时results已经完全填充,安全读取 Assert.IsTrue(results.OrderBy(x => x).SequenceEqual(Enumerable.Range(0, 30))); }
当然,如果后续你需要让ActionBlock并行处理消息(比如把MaxDegreeOfParallelism设为3),那此时多个线程会同时调用results.Add,List<int>就会出现线程安全问题,这时候换成BlockingCollection<int>或者ConcurrentBag<int>才是合理的选择。但就当前的测试代码而言,List<int>完全够用。
内容的提问来源于stack exchange,提问作者Oleg Ivanov

