假设我有
Dictionary<string,IEnumerable<int>>
和:
Task1 尝试替换某个键下的值。
Task2 循环遍历同一 key 下的 value 元素。
这个线程安全吗?我认为如果我首先获取该值然后在 Task2 中循环它应该是这样。在这种情况下,Task2 应该获取旧值或新值,但我认为它永远不应该生成任何线程异常。
在讨论线程安全时,您需要“非常”精确和具体。简短的回答“不”。更长的答案“可能是,如果你努力的话,但最好重新设计一下”。 就字典而言,如果我们假设你正在改变字典:我们需要考虑 get 与 set 的竞争。这不是线程安全的,除非您对各个操作进行同步 - 如果至少有一个访问是突变,那么在同时访问字典时您“绝对”会看到异常。但坦率地说:使用
ConcurrentDictionary<,>
来代替可能更有意义。 另外,我们需要考虑序列的枚举,
IEnumerable<int>
。现在; IEnumerable<int>
not保证可重复,并且它在查找中的事实意味着您可能计划多次枚举它,所以:这并不理想。许多实现
IEnumerable<T>
的类型是可重复的,但这不是 API 保证。此外:其中许多类型本身是可变的,因此与这些类型的更改相比,不一定是线程安全的。所以:如果是我,我可能会考虑使用ConcurrentDictionary<string, ImmutableArray<int>>
或ConcurrentDictionary<string, ReadOnlyMemory<int>>
,然后我就有信心它是安全的...
...只要没有人使用反射或不安全代码,在这种情况下,所有的赌注都会失败!我喜欢这个
线程安全如果程序或方法不存在不确定性,则该程序或方法是线程安全的 面对任何多线程场景。
因此,如果您用替换该值,您可以得到一个
null
NullReferenceException
,在单线程场景中您不会期望它(有点做作,但 ContainsKey 已经足够频繁使用)
var dict = new Dictionary<string, IEnumerable<int>>();
dict["foo"] = new List<int>() { 1, 2, 3 };
var mres = new ManualResetEventSlim(false);
Task.Run(() => {
if (dict.ContainsKey("foo") && dict["foo"] != null) {
mres.Wait();
foreach (var element in dict["foo"]) {
Console.WriteLine(element);
}
}
});
Thread.Sleep(100);
Task.Run(() => {
mres.Set();
dict["foo"] = null;
});
另一种情况是,如果您使用外部和内部 foreach
循环,并且外部循环对旧数据进行操作,内部循环对新数据进行操作,您希望得到的是same
数据。
我确信还有更多例子可以说明为什么这不是“线程安全”的。