c# - 在多线程应用程序中同步属性值的正确方法
问题描述
我最近开始重新审视我的一些旧的多线程代码,并想知道它是否安全且正确(生产中还没有问题......)。特别是我是否正确处理对象引用?我已经阅读了大量使用简单原语(如整数)的示例,但与引用和任何可能的细微差别有关的例子并不多。
首先,我最近了解到对象引用分配是原子的,至少在 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; } } };
}
现在我知道引用分配是原子的,我想“太好了,是时候移除这些丑陋且不必要的锁了!” 然后我进一步阅读并了解了线程之间的内存同步。现在我又开始保留锁以确保数据在访问时不会过时。例如,如果我访问联系人的选项,我想确保我始终收到最新分配的一组选项。
问题:
- 如果我在这里错了,请纠正我,但是上面的代码确实确保了当我以线程安全的方式获取 Options 时,我实现了获取最新值的目标?使用此方法还有其他问题吗?
- 我相信锁有一些开销(转换为 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 类是故意不可变的,因为我不想同步或担心选项本身的原子性。它们可能包含任何类型的数据类型,因此我认为在需要更改时分配一组新的选项会更干净/更安全。
- 我熟悉获取一个值、使用该值、然后设置该值的非原子含义。考虑以下代码段:
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;
}
解决方案
如果我在这里错了,请纠正我,但是上面的代码确实确保了当我以线程安全的方式获取 Options 时,我实现了获取最新值的目标?使用此方法还有其他问题吗?
是的,锁会发出内存屏障,因此它会确保从内存中读取值。除了可能比它必须的更保守之外,没有其他真正的问题。但我有一句话,如果有疑问,请使用锁。
我相信锁有一些开销(转换为 Monitor.Enter/Exit)。我认为我可以使用 Interlocked 来获得名义上的性能提升,但对我来说更重要的是,一组更干净的代码。以下工作可以实现同步吗?
互锁也应该发出内存屏障,所以我认为这应该或多或少做同样的事情。
由于引用分配是原子的,在分配引用时是否需要同步(使用锁或互锁)?如果我省略了set逻辑,只维护get,我还会保持原子性和同步吗?我有希望的想法是 get 中的锁/互锁使用将提供我正在寻找的同步。我已经尝试编写示例程序来强制使用陈旧的值场景,但我无法可靠地完成它。
我认为在这种情况下,仅使该字段 volatile 就足够了。据我了解,“陈旧值”的问题有些夸张,缓存一致性协议应该解决大多数问题。
据我所知,主要问题是阻止编译器只是将值放入寄存器而不进行任何后续加载。这volatile
应该可以防止这种情况,强制编译器在每次读取时发出负载。但这在重复检查循环中的值时主要是一个问题。
但是只看一个属性并不是很有用。当您有多个需要同步的值时,问题经常会出现。一个潜在的问题是编译器或处理器对指令重新排序。锁和内存屏障可以防止这种重新排序,但如果这是一个潜在的问题,最好锁定更大的代码部分。
总的来说,我认为在处理多个线程时偏执是谨慎的做法。使用多同步可能比使用少更好。一个例外是死锁可能是由于拥有太多锁而导致的。我对此的建议是在持有锁时要非常小心你所调用的内容。理想情况下,锁应该只持有很短的、可预测的时间。
还要继续使用纯函数和不可变数据结构。这些是避免担心线程问题的好方法。
推荐阅读
- python - Python - 使用 pandas.plot.hist() 时如何对浮点数进行 bin 处理
- css - Tailwind CSS 如何在 React 中设置 href 链接的样式?
- javascript - 如何在 Google Analytics 目标中通过 Add To Cart 事件的事件值?
- django - 我以与用户表的一对一关系创建了一个额外的表额外表。如何在用户注册中显示电话字段
- c++ - C++ 替换:用 v.at(x) 替换每次出现的 v[x]
- python - 如何更改表单 CharField 中的默认空代表?
- android - 如何检查谷歌地图是否仅由用户手势(平移,倾斜,捏......)而不是以编程方式移动
- pytorch - 运行 pytorch 几何获取 CUDA 错误:调用 `cublasCreate(handle)` 时出现 CUBLAS_STATUS_NOT_INITIALIZED
- sql-server - SSAS(表格模型)和 SSRS 之间的超时
- python - 在 Python 中将 csv 文件解析为布尔表达式