C#从一个线程设置字典对象是否线程安全,从另一个线程获取
C# Is it threadsafe to set dictionary object from one thread, get from other thread
假设我们有一些 strange class 其中包含
- public 属性
ReadOnlyDictionary<string, string> Map {get; private set;}
- 方法
Update
在调用时重新设置 Map
字典。
从一个线程调用方法 Update
并从另一个线程获取 Map
是否线程安全?
public class Example
{
public ReadOnlyDictionary<string, string> Map{ get; private set; }
public void Update(IEnumerable<KeyValuePair<string, string>> items)
{
Map = items
.ToLookup(x => x.Key)
.ToDictionary(x => x.Key, x => x.First())
.ToReadOnlyDictionary(); // helper method
}
}
是否有可能会出现我们调用 Map getter 而它 returns 为 null 或处于不良状态的情况,因为 setter 执行不是原子的?
"Thread safe" 是一个模糊的术语。维基百科将 thread-safe 代码定义为:
Thread-safe code only manipulates shared data structures in a manner
that ensures that all threads behave properly and fulfill their design
specifications without unintended interaction
所以我们不能在不知道设计规范的情况下说某段代码是 "thread safe",其中包括这段代码的实际使用方式。
从某种意义上说,这段代码是 "thread-safe",任何线程都不可能观察到处于损坏或部分状态的 Example
对象。
引用分配是原子的,因此读取 Map
的线程只能读取它的一个版本或另一个版本,它不能观察 Map
的部分或中间状态。因为 Map
的键和值都是不可变的(字符串)——整个对象是不可变的,并且在从 Map
属性.[=30= 中读取后可以安全地被多个线程使用]
然而,做这样的事情:
var example = GetExample();
if (example.Map.ContainsKey("key")) {
var key = example.Map["key"];
}
当然不是线程安全的,因为 Map
对象作为一个整体,可能在检查和从字典中读取之间发生了变化,旧版本包含 "key" 键,但新版本版本没有。另一方面这样做:
var example = GetExample();
var map = example.Map;
if (map.ContainsKey("key")) {
var key = map["key"];
}
很好。任何不依赖 example.Map
在读取之间保持相同的代码都应该是 "safe".
因此您必须确保给定代码在您的特定用例中是 "thread safe"。
注意这里使用ConcurrentDictionary
是完全没用的。由于您的字典是只读的 - 从多个线程读取它已经很安全了。
更新:评论中提出的有效观点是 ReadOnlyDictionary
本身并不能保证基础数据不被修改,因为 ReadOnlyDictionary
只是常规可变 Dictionary
的包装器.
但是,在这种情况下,您在 Update
方法中从 non-shared 字典实例(从 ToDictionary
接收)创建 ReadOnlyDictionary
,它不用于任何其他用途除了将它包装在 ReadOnlyDictionary
中。这是一种安全的用法,因为除了 ReadOnlyDictionary
本身之外,没有任何代码可以访问可变的底层字典,并且只读字典会阻止其修改。只要没有写入,即使是常规的 Dictionary
读取也是线程安全的。
because of setter execution is not atomic
此代码的安全性与原子性无关(read/write 引用是 .NET 中的原子操作,除非你搞砸了对齐)。
因此,此代码中唯一的问题是内存重新排序。另一个线程可能会获得对部分构造的对象的引用。
但它可能只发生在内存模型较弱的硬件上('weak' 意味着允许大量重新排序并且没有太多限制)。
为防止这种情况,您只需通过 Volatile.Write
发布此参考,对于 x86 和 x86_64 架构是 nop
,对于 ARM 和其他架构是一种内存栅栏。
为了使您的代码更简单和更少 bug-prone 您可以使用 ImmutableDictionary:
public class Example
{
public IReadOnlyDictionary<string, string> Map{ get; private set; } =
ImmutableDictionary.Create<string, string>();
public void Update(IEnumerable<KeyValuePair<string, string>> items)
{
Map = items
.ToImmutableDictionary();
}
}
ImmutableDictionary 保证是线程安全的,因此无法破坏它。通常的 Dictionary 不是线程安全的,但是在您的实现中不应该有任何线程安全问题,但这需要这种特殊用法。
ImmutableDictionary 可以在这里下载 https://www.nuget.org/packages/System.Collections.Immutable/
另一个 thread-safe 字典是 ConcurrentDictionary。使用它不一定会产生额外的成本,因为读取操作是 lock-free。文档指出:
Read operations on the dictionary are performed in a lock-free manner.
所以这两个字典在任何情况下都是 thread-safe(在字典 API 级别)并且 Dictionary 在某些实现中是 thread-safe你的。
并发集合 = 线程安全:)
我认为你应该使用并发收集。并发泛型对我来说很有意义。
假设我们有一些 strange class 其中包含
- public 属性
ReadOnlyDictionary<string, string> Map {get; private set;}
- 方法
Update
在调用时重新设置Map
字典。
从一个线程调用方法 Update
并从另一个线程获取 Map
是否线程安全?
public class Example
{
public ReadOnlyDictionary<string, string> Map{ get; private set; }
public void Update(IEnumerable<KeyValuePair<string, string>> items)
{
Map = items
.ToLookup(x => x.Key)
.ToDictionary(x => x.Key, x => x.First())
.ToReadOnlyDictionary(); // helper method
}
}
是否有可能会出现我们调用 Map getter 而它 returns 为 null 或处于不良状态的情况,因为 setter 执行不是原子的?
"Thread safe" 是一个模糊的术语。维基百科将 thread-safe 代码定义为:
Thread-safe code only manipulates shared data structures in a manner that ensures that all threads behave properly and fulfill their design specifications without unintended interaction
所以我们不能在不知道设计规范的情况下说某段代码是 "thread safe",其中包括这段代码的实际使用方式。
从某种意义上说,这段代码是 "thread-safe",任何线程都不可能观察到处于损坏或部分状态的 Example
对象。
引用分配是原子的,因此读取 Map
的线程只能读取它的一个版本或另一个版本,它不能观察 Map
的部分或中间状态。因为 Map
的键和值都是不可变的(字符串)——整个对象是不可变的,并且在从 Map
属性.[=30= 中读取后可以安全地被多个线程使用]
然而,做这样的事情:
var example = GetExample();
if (example.Map.ContainsKey("key")) {
var key = example.Map["key"];
}
当然不是线程安全的,因为 Map
对象作为一个整体,可能在检查和从字典中读取之间发生了变化,旧版本包含 "key" 键,但新版本版本没有。另一方面这样做:
var example = GetExample();
var map = example.Map;
if (map.ContainsKey("key")) {
var key = map["key"];
}
很好。任何不依赖 example.Map
在读取之间保持相同的代码都应该是 "safe".
因此您必须确保给定代码在您的特定用例中是 "thread safe"。
注意这里使用ConcurrentDictionary
是完全没用的。由于您的字典是只读的 - 从多个线程读取它已经很安全了。
更新:评论中提出的有效观点是 ReadOnlyDictionary
本身并不能保证基础数据不被修改,因为 ReadOnlyDictionary
只是常规可变 Dictionary
的包装器.
但是,在这种情况下,您在 Update
方法中从 non-shared 字典实例(从 ToDictionary
接收)创建 ReadOnlyDictionary
,它不用于任何其他用途除了将它包装在 ReadOnlyDictionary
中。这是一种安全的用法,因为除了 ReadOnlyDictionary
本身之外,没有任何代码可以访问可变的底层字典,并且只读字典会阻止其修改。只要没有写入,即使是常规的 Dictionary
读取也是线程安全的。
because of setter execution is not atomic
此代码的安全性与原子性无关(read/write 引用是 .NET 中的原子操作,除非你搞砸了对齐)。
因此,此代码中唯一的问题是内存重新排序。另一个线程可能会获得对部分构造的对象的引用。
但它可能只发生在内存模型较弱的硬件上('weak' 意味着允许大量重新排序并且没有太多限制)。
为防止这种情况,您只需通过 Volatile.Write
发布此参考,对于 x86 和 x86_64 架构是 nop
,对于 ARM 和其他架构是一种内存栅栏。
为了使您的代码更简单和更少 bug-prone 您可以使用 ImmutableDictionary:
public class Example
{
public IReadOnlyDictionary<string, string> Map{ get; private set; } =
ImmutableDictionary.Create<string, string>();
public void Update(IEnumerable<KeyValuePair<string, string>> items)
{
Map = items
.ToImmutableDictionary();
}
}
ImmutableDictionary 保证是线程安全的,因此无法破坏它。通常的 Dictionary 不是线程安全的,但是在您的实现中不应该有任何线程安全问题,但这需要这种特殊用法。
ImmutableDictionary 可以在这里下载 https://www.nuget.org/packages/System.Collections.Immutable/
另一个 thread-safe 字典是 ConcurrentDictionary。使用它不一定会产生额外的成本,因为读取操作是 lock-free。文档指出:
Read operations on the dictionary are performed in a lock-free manner.
所以这两个字典在任何情况下都是 thread-safe(在字典 API 级别)并且 Dictionary 在某些实现中是 thread-safe你的。
并发集合 = 线程安全:)
我认为你应该使用并发收集。并发泛型对我来说很有意义。