关于ConcurrentBag<T>中TryTake窃取元素行为的理解误区纠正
纠正对ConcurrentBag窃取元素逻辑的理解偏差
在讲解ConcurrentBag<T>时,曾看到这样的描述:
...准确来说,调用
Take会获取当前线程最近添加的元素;若当前线程无元素,则随机获取其他线程最近添加的元素。
但实际查看Visual Studio中TrySteal方法(TryTake内部调用该方法)的注释,逻辑是:
若当前线程无本地队列,则从头队列开始尝试从每个队列窃取元素,直到获取结果。若有本地队列,则从其下一个队列开始遍历,回到当前队列时停止(不包含自身)。
测试代码
using static System.Console; using System.Collections.Concurrent; IProducerConsumerCollection<Car> cars = new ConcurrentBag<Car>(); var addBlackCars = Task.Run(ProcessBlackCarModels); var addNonBlackCars = Task.Run(ProcessNonBlackCarModels); Task.WaitAll(addBlackCars, addNonBlackCars); WriteLine($"At present, the repository contains {cars.Count} car(s)."); void ProcessNonBlackCarModels() { Car car; car = new("Hyundai Creta", "Pearl"); WriteLine($"Adding: {car} using task-{Task.CurrentId}"); cars.TryAdd(car); Thread.Sleep(1000); car = new("Maruti Suzuki Alto 800", "Red"); WriteLine($"Adding: {car} using task-{Task.CurrentId}"); cars.TryAdd(car); Thread.Sleep(1000); car = new("Toyota Fortuner Avant", "Bronze"); WriteLine($"Adding: {car} using task-{Task.CurrentId}"); cars.TryAdd(car); Thread.Sleep(1000); WriteLine($"Task-{Task.CurrentId} will try removing one item now."); if (cars.Count > 0) { cars.TryTake(out Car removeCar); WriteLine($"Tried removing: {removeCar} using task-{Task.CurrentId}"); } } void ProcessBlackCarModels() { Car car; car = new("Toyota Fortuner Attitude", "Black"); WriteLine($"Adding: {car} using task-{Task.CurrentId}"); cars.TryAdd(car); Thread.Sleep(1000); car = new("Hyundai Creta Abyss", "Black"); WriteLine($"Adding: {car} using task-{Task.CurrentId}"); cars.TryAdd(car); // Putting a relatively long sleep so that the other task can finish in between. Thread.Sleep(5000); WriteLine($"Task-{Task.CurrentId} will try removing three items now."); for (int i = 0; i < 3; i++) { if (cars.Count > 0) { cars.TryTake(out Car removeCar); WriteLine($"Tried removing: {removeCar} using task-{Task.CurrentId}"); } } } // Using primary constructor class Car(string model, string color) { private string _model = model; private string _color = color; public override string ToString() { return $"[{_model}, {_color}]"; } }
示例输出
Adding: [Toyota Fortuner Attitude, Black] using task-1 Adding: [Hyundai Creta, Pearl] using task-2 Adding: [Maruti Suzuki Alto 800, Red] using task-2 Adding: [Hyundai Creta Abyss, Black] using task-1 Adding: [Toyota Fortuner Avant, Bronze] using task-2 Task-2 will try removing one item now. Tried removing: [Toyota Fortuner Avant, Bronze] using task-2 Task-1 will try removing three items now. Tried removing: [Hyundai Creta Abyss, Black] using task-1 Tried removing: [Toyota Fortuner Attitude, Black] using task-1 Tried removing: [Hyundai Creta, Pearl] using task-1 At present, the repository contains 1 car(s).
理解偏差纠正
从输出可以看到,task-1在窃取第三个元素时,移除了task-2最早添加的元素[Hyundai Creta, Pearl],而非最近添加的[Maruti Suzuki Alto 800, Red]。
ConcurrentBag<T>的核心逻辑是:
- 每个线程拥有独立的本地元素队列,线程自身取元素时遵循**后进先出(LIFO)**规则,优先拿自己最近添加的元素;
- 当线程需要窃取其他线程的元素时,会遍历目标线程的队列,并从队列**头部(最早添加的元素)获取,遵循先进先出(FIFO)**规则。
之前的描述简化了窃取逻辑,实际并非“随机获取其他线程最近添加的元素”,而是按固定顺序遍历其他线程队列,并从队列头部窃取元素。这就是为什么task-1窃取task-2的元素时,先拿到的是task-2最早添加的元素,而task-2自己取元素时拿到的是最近添加的元素。
内容的提问来源于stack exchange,提问作者Vaskaran Sarcar
相关产品推荐
相关产品推荐

