目录
前言
线程安全
GetOrAdd
真是这样吗?
总结
前言最近,看到一篇文章,讲到《ConcurrentDictionary字典操作竟然不全是线程安全的?》。
首先,这个结论是正确的,但文中给出的一个证明例子,我觉得是有问题的。
相关代码如下:
using System.Collections.Concurrent;
public class Program
{
private static int _runCount = 0;
private static readonly ConcurrentDictionary _dictionary
= new ConcurrentDictionary();
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 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 方法不是线程安全的。
让我们再把 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
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?