在解释
ConcurrentBag<T>
时,在线链接 https://www.albahari.com/threading/part5.aspx 表示“...因此,准确地说,调用 Take 会为您提供最近添加的元素该线程;如果该线程上没有元素,它会为您提供最近在另一个线程上添加的元素,随机选择。”
事实上,在VS中,如果你深入的话,你会看到
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).
请注意,任务 1 需要窃取此输出中的第三个项目,它通过删除任务 2 在开头添加的项目(即最近添加的项目)来实现此目的。 我的理解是:任务 1 应该删除最近添加的任务 2 的项目,但不是最近添加的项目。请帮我纠正这个理解。
我查看了 Concurrentbag::ThreadLocalListcode 的 GitHub 存储库,并查看了下面的窃取代码,它与您的输出一致。
在 .NET 中的 ConcurrentBag 类的上下文中,项目被添加到每个线程的本地列表的头部或从每个线程的本地列表的头部获取。这意味着每个线程的本地列表的行为类似于堆栈(后进先出 - LIFO)。最近添加的项目(列表的“头”)将是同一线程第一个获取的项目。
当一个线程因为本地列表为空而需要窃取一个项目时,它会从另一个线程本地列表的尾部获取。这意味着它窃取了另一个线程最近最少添加的项目。这背后的原因是,另一个线程不太可能在不久的将来获取其最近最少添加的项目,因为它以 LIFO 方式获取自己的项目(从其列表的头部开始)。
/// <summary>
/// Steal an item from the tail of the list
/// </summary>
/// <param name="result">the removed item</param>
/// <param name="remove">remove or peek flag</param>
internal void Steal(out T result, bool remove)
{
Node tail = m_tail;
Debug.Assert(tail != null);
if (remove) // Take operation
{
m_tail = m_tail.m_prev;
if (m_tail != null)
{
m_tail.m_next = null;
}
else
{
m_head = null;
}
// Increment the steal count
m_stealCount++;
}
result = tail.m_value;
}