如何证明 ConcurrentDictionary 字典操作"不全是"线程安全的
Posted dotNET跨平台
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何证明 ConcurrentDictionary 字典操作"不全是"线程安全的相关的知识,希望对你有一定的参考价值。
前言
最近,看到一篇文章,讲到《ConcurrentDictionary字典操作竟然不全是线程安全的?》。
首先,这个结论是正确的,但文中给出的一个证明例子,我觉得是有问题的。
相关代码如下:
using System.Collections.Concurrent;
public class Program
private static int _runCount = 0;
private static readonly ConcurrentDictionary<string, string> _dictionary
= new ConcurrentDictionary<string, string>();
public static void Main(string[] args)
var task1 = Task.Run(() => PrintValue("The first value"));
var task2 = Task.Run(() => PrintValue("The second value"));
var task3 = Task.Run(() => PrintValue("The three value"));
var task4 = Task.Run(() => PrintValue("The four value"));
Task.WaitAll(task1, task2, task4,task4);
PrintValue("The five value");
Console.WriteLine($"Run count: _runCount");
public static void PrintValue(string valueToPrint)
var valueFound = _dictionary.GetOrAdd("key",
x =>
Interlocked.Increment(ref _runCount);
Thread.Sleep(100);
return valueToPrint;
);
Console.WriteLine(valueFound);
那这个例子是不是能够说明 ConcurrentDictionary 字典操作不是线程安全的呢?
首先,让我们看看什么是“线程安全”。
线程安全
线程安全:当多个线程同时访问时,保证实现没有争用条件。
这里的“争用条件”又是什么呢?下面举个例子来说明。
假设两个线程各自将全局整数变量的值递增 1。理想情况下,将发生以下操作序列:
线程 1 | 线程 2 | 整数值 | |
---|---|---|---|
0 | |||
读取值 | ← | 0 | |
增加值 | 0 | ||
回写 | → | 1 | |
读取值 | ← | 1 | |
增加值 | 1 | ||
回写 | → | 2 |
在上面显示的情况下,最终值为 2,如预期的那样。但是,如果两个线程在没有锁定或同步的情况下同时运行,则操作的结果可能是错误的。下面的替代操作序列演示了此方案:
线程 1 | 线程 2 | 整数值 | |
---|---|---|---|
0 | |||
读取值 | ← | 0 | |
读取值 | ← | 0 | |
增加值 | 0 | ||
增加值 | 0 | ||
回写 | → | 1 | |
回写 | → | 1 |
在这种情况下,最终值为 1,而不是预期的结果 2。发生这种情况是因为此处的增量操作不是互斥的。互斥操作是在访问某些资源(如内存位置)时无法中断的操作。
如果用那篇文章的例子,演示是否线程安全的代码应该是这样的:
using System.Collections.Concurrent;
public class Program
private static int _runCount = 0;
private static int _notsafeCount = 0;
public static void Main(string[] args)
var tasks = new Task[100];
for (int i = 0; i < tasks.Length; i++)
tasks[i] = Task.Run(() => PrintValue($"The i value"));
Task.WaitAll(tasks);
Console.WriteLine($"Run count: _runCount");
Console.WriteLine($"Not Safe Count: _notsafeCount");
public static void PrintValue(string valueToPrint)
Interlocked.Increment(ref _runCount);
_notsafeCount++;
Thread.Sleep(100);
我们把 Task 数量加大到 100,便于查看效果。
执行 3 次,_runCount 始终等于 100,因为Interlocked是线程安全的,而 _notsafeCount 的值却是随机的,说明 PrintValue 方法不是线程安全的。
GetOrAdd
让我们再把 PrintValue 方法改成使用 GetOrAdd:
public static void PrintValue(string valueToPrint)
var valueFound = _dictionary.GetOrAdd("key",
x =>
Interlocked.Increment(ref _runCount);
_notsafeCount++;
Thread.Sleep(100);
return valueToPrint;
);
Console.WriteLine(valueFound);
再执行 3 次,我们发现,_notsafeCount 的值始终和 _runCount 的值相同,貌似没出现线程争用。
大家看到这是不是有点懵逼,这不反而证明了,
ConcurrentDictionary字典操作是线程安全的!
真是这样吗?
这也正是我认为原文的例子不太恰当的原因:它只证明了有多个线程进入,而没证明出现了线程争用,无法得到线程不安全的结论。
从上面线程不安全的例子我们看到,一共 100 个 Task 执行而 _notsafeCount 的值都是 90 多,这说明线程争用很难被触发。而上面的操作只执行了 8 次,也许是还没触发线程争用呢?
我们修改代码,每进入 1 次 valueFactory 就执行 10 次 _notsafeCount++:
public static void PrintValue(string valueToPrint)
var valueFound = _dictionary.GetOrAdd("key",
x =>
Interlocked.Increment(ref _runCount);
for (int i = 0; i < 10; i++)
_notsafeCount++;
Thread.Sleep(100);
return valueToPrint;
);
Console.WriteLine(valueFound);
理论上,_notsafeCount 应该等于 90(9*10),而实际上输出 88,这说明出现了线程争用。
也就是说,ConcurrentDictionary 的 GetOrAdd(TKey key, Func<TKey, TValue> valueFactory) 方法不是线程安全的。
这个结论从 GetOrAdd 方法的源码也可以得到验证,执行 valueFactory(key) 时是没加锁的:
public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
if (key is null)
ThrowHelper.ThrowKeyNullException();
if (valueFactory is null)
ThrowHelper.ThrowArgumentNullException(nameof(valueFactory));
IEqualityComparer<TKey>? comparer = _comparer;
int hashcode = comparer is null ? key.GetHashCode() : comparer.GetHashCode(key);
if (!TryGetValueInternal(key, hashcode, out TValue? resultingValue))
TryAddInternal(key, hashcode, valueFactory(key), updateIfExists: false, acquireLock: true, out resultingValue);
return resultingValue;
总结
如果你想验证某个方法是否线程安全,都可以用上面这种触发线程争用方式。
还不赶紧试试?!
添加微信号【MyIO666】,邀你加入技术交流群
以上是关于如何证明 ConcurrentDictionary 字典操作"不全是"线程安全的的主要内容,如果未能解决你的问题,请参考以下文章
.NET 锁定还是 ConcurrentDictionary?
.NET - 字典锁定与 ConcurrentDictionary