【发布时间】:2021-12-27 21:08:51
【问题描述】:
我最近开始重新审视我的一些旧的多线程代码,并想知道它是否安全且正确(生产中还没有问题......)。特别是我是否正确处理对象引用?我已经阅读了大量使用简单原语(如整数)的示例,但与引用和任何可能的细微差别有关的例子并不多。
首先,我最近了解到对象引用分配是原子的,至少在 64 位机器上是我针对这个特定应用程序所关注的全部内容。以前,我锁定类属性的 get/sets 以避免破坏引用,因为我没有意识到引用分配是原子的。 例如:
// Immutable collection of options for a Contact
public class ContactOptions
{
public string Email { get; }
public string PhoneNumber { get; }
}
// Sample class that implements the Options
public class Contact
{
private readonly object OptionsLock = new object();
private ContactOptions _Options;
public ContactOptions Options { get { lock(OptionsLock) { return _Options; } }
set { lock(OptionsLock) { _Options = value; } } };
}
现在我知道引用分配是原子的,我想“太好了,是时候移除这些丑陋且不必要的锁了!” 然后我进一步阅读并了解了线程之间的内存同步。现在我又开始保留锁以确保数据在访问时不会过时。例如,如果我访问联系人的选项,我想确保我始终收到最新分配的一组选项。
问题:
- 如果我在这里错了,请纠正我,但上面的代码确实可以确保当我以线程安全的方式获取选项时,我实现了获取选项的最新值的目标?使用此方法还有其他问题吗?
- 我相信锁存在一些开销(转换为 Monitor.Enter/Exit)。我认为我可以使用 Interlocked 来获得名义上的性能提升,但对我来说更重要的是,一组更干净的代码。以下是否可以实现同步?
private ContactOptions _Options;
public ContactOptions Options {
get { return Interlocked.CompareExchange(ref _Options, null, null); }
set { Interlocked.Exchange(ref _Options, value); } }
- 由于引用分配是原子的,分配引用时是否需要同步(使用锁或互锁)?如果我省略了set逻辑,只维护get,我还会保持原子性和同步吗?我有希望的想法是 get 中的锁/互锁使用将提供我正在寻找的同步。我曾尝试编写示例程序来强制使用陈旧的值场景,但我无法可靠地完成它。
private ContactOptions _Options;
public ContactOptions Options {
get { return Interlocked.CompareExchange(ref _Options, null, null); }
set { _Options = value; } }
旁注:
- ContactOptions 类是故意不可变的,因为我不想同步或担心选项本身的原子性。它们可能包含任何类型的数据类型,因此我认为在需要更改时分配一组新的选项会更干净/更安全。
- 我熟悉获取一个值、使用该值,然后设置该值的非原子含义。考虑以下 sn-p:
public class SomeInteger
{
private readonly object ValueLock = new object();
private int _Value;
public int Value { get { lock(ValueLock) { return _Value; } }
private set { lock(ValueLock) { _Value = value; } } };
// WRONG
public void manipulateBad()
{
Value++;
}
// OK
public void manipulateOk()
{
lock (ValueLock)
{
Value++;
// Or, even better: _Value++; // And remove the lock around the setter
}
}
}
重点是,我真的只关注内存同步问题。
解决方案: 我选择了 Volatile.Read 和 Volatile.Write 方法,因为它们确实使代码更明确,它们比 Interlocked 和 lock 更干净,而且比前面提到的更快。
// Sample class that implements the Options
public class Contact
{
public ContactOptions Options { get { return Volatile.Read(ref _Options); } set { Volatile.Write(ref _Options, value); } }
private ContactOptions _Options;
}
【问题讨论】:
-
你可能对这个Eric Lippert answer about
volatile感兴趣。 -
@JohnWu 谢谢,这种担忧正是我一直避免使用 volatile 的原因。我选择了 Volatile.Read/Write 以确保内存屏障满足我的需要,更明确,并且比 Interlocked 执行得更好,并且绝对比 lock 更快
-
Volatility 是不够的,因为 volatile 不会对写入进行排序。处理器 1 创建一个 ContactOptions 并将引用写入内存。但是 ContactOptions 的内容仍然位于 L1 缓存中,并且不会刷新到内存中。处理器 2 读取引用并尝试访问 ContactOptions 并获取未初始化的数据,因为处理器 1 尚未将其写出。或者处理器 2 可能会使用其自己的 L1 高速缓存中的内存,而不是从内存中读取。在写入之前需要一个释放屏障,在读取之前需要一个获取屏障。
标签: c# multithreading locking thread-synchronization interlocked