问题很简单:为什么线程安全字典允许数据竞争,因为该字典中的所有方法都是原子的,并且两个线程不应该接收相同的值?
在下面的示例中,可以显示至少10个、至少12个、至少14个。
public class Program
{
private static ConcurrentDictionary<string, int> _map = [];
static async Task Main(string[] args)
{
_map["air"] = 0;
var tasks = new List<Task>();
for (int i = 0; i < 10000; i++)
{
tasks.Add(Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
_map.TryGetValue("air", out int value);
_map.AddOrUpdate("air", value, (key, oldValue) => oldValue + 1);
_map.TryGetValue("air", out int newValue);
if (newValue >= 10)
{
_map.TryRemove("air", out _);
_map.TryAdd("air1", newValue);
}
}
}));
}
await Task.WhenAll(tasks);
Console.WriteLine(_map["air1"]);
}
}
字典负责在每个单独的操作中访问它时的线程安全,并且您有多个操作。操作之间任何事情都可能发生。此外,lambda
AddOrUpdate不是在锁定下执行的,也不是原子操作的一部分。顺便说一句,您可能会感到困惑,但所有这些循环都没有意义,因为
_map.TryAdd("air1", newValue);它们在程序的整个执行过程中只会工作一次,即当键不在字典中时。我假设您想要对这个键中的值求和,因此下面的代码考虑了此错误的修复。您可以解决字典之外的计算,即从那里获取一个值,用它做任何您需要做的事情,然后返回它。
大概如此
这种迷你循环称为自旋锁。自旋锁的目的是在估计其持续时间很短并且迭代次数很少时确保线程安全。理想情况下,自旋锁的持续时间应使处理器上的负载在其余应用程序代码的总资源消耗的测量误差范围内,即不可见。
这段代码运行大约一秒钟。也就是说,比从 0 到 100000 的循环要慢得多。但是 99% 的时间不是花在计算上,甚至不是花在自旋锁上,而是花在 10,000 个线程的上下文启动和切换上。
这段代码很酷的一点是,线程可以处理彼此的值,即在调用字典之间,不同的线程可以执行计算,即相互交换数据,并且结果在 100% 的情况下都是正确的。
事实上,你需要更加小心自旋锁,让我们来计算一下点击次数。
太多了
我通过实验发现,如果在最后一个周期添加最小等待时间,情况就会改变。
事实证明
这是完全不同的事情。这可以认为是一个稳定的解决方案。
解决方案是通过 .NET 内置的框架
SpinWait,它比循环更聪明一些它的工作速度也非常快,有效地减少了碰撞的可能性。