在多线程应用程序中同步 属性 值的正确方法
Proper way to synchronize a property's value in a multi-threaded application
我最近开始重新审视我的一些旧的多线程代码,想知道它们是否安全和正确(生产中还没有问题...)。特别是我是否正确处理了对象引用?我已经阅读了大量使用整数等简单基元的示例,但与引用和任何可能的细微差别有关的示例并不多。
首先,我最近了解到对象引用分配是原子的,至少在 64 位机器上是这样,这是我针对这个特定应用程序所关注的全部内容。以前,我锁定 class 属性的 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 的最新值时实现了获取 Options 的最新值的目标?使用此方法还有其他问题吗?
- 我相信锁有一些开销(转换为监视器。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 中的 lock/Interlock 用法将提供我正在寻找的同步。我已经尝试编写示例程序来强制执行陈旧的值场景,但我无法可靠地完成它。
private ContactOptions _Options;
public ContactOptions Options {
get { return Interlocked.CompareExchange(ref _Options, null, null); }
set { _Options = value; } }
旁注:
- ContactOptions class 故意不可变,因为我不想同步或担心选项本身的原子性。它们可能包含任何类型的数据,所以我认为在需要更改时分配一组新的选项是很多 cleaner/safer。
- 我熟悉获取值、使用该值然后设置该值的非原子含义。考虑以下片段:
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;
}
Correct me if I'm wrong here, but the above code does ensure that I'm achieving the goal of getting the latest value of Options when I get it in a thread safe manner? Any other issues using this method?
是的,锁会发出内存屏障,因此它会确保从内存中读取值。除了可能比必须的更保守之外,没有真正的问题。但是我有一句话,如果有疑问,就用一把锁。
I believe there is some overhead with the lock (Converts to Monitor.Enter/Exit). I thought I could use Interlocked for a nominal performance gain, but more importantly to me, a cleaner set of code. Would the following work to achieve synchronization?
Interlocked 也应该发出内存屏障,所以我认为这应该或多或少做同样的事情。
Since a reference assignment is atomic, is the synchronization (using either lock or Interlocked) necessary when assigning the reference? If I omit the set logic and only maintain the get, will I still maintain atomicity and synchronization? My hopeful thinking is that the lock/Interlock usage in the get would provide the synchronization I'm looking for. I've tried writing sample programs to force stale value scenarios, but I couldn't get it done reliably.
我认为在这种情况下,只要让字段变易变就足够了。据我了解,“过时值”的问题有些夸张,缓存一致性协议应该可以解决大多数问题。
据我所知,主要问题是阻止编译器只将值放入寄存器而不进行任何后续加载。并且 volatile
应该可以防止这种情况,迫使编译器在每次读取时发出加载。但这主要是在循环中重复检查值时出现的问题。
但只看一个属性用处不大。当您有多个需要同步的值时,问题会更频繁地出现。一个潜在的问题是编译器或处理器对指令重新排序。锁和内存屏障可以防止这种重新排序,但如果这是一个潜在的问题,最好锁定更大的代码段。
总的来说,我认为在处理多线程时偏执是谨慎的做法。使用更多的同步可能比使用更少的同步更好。一种例外情况是锁太多可能导致的死锁。我对此的建议是在持有锁时要非常小心你所调用的内容。理想情况下,锁应该只持有一个短的、可预测的时间。
还要继续使用纯函数和不可变数据结构。这些是避免担心线程问题的好方法。
- 是的,
lock (OptionsLock)
确保所有线程将看到 Options
的最新值,因为 memory barriers 在进入和退出 lock
时被插入。
- 将
lock
替换为 Interlocked
或 Volatile
class 的方法同样可以很好地实现最新值可见性目标。这些方法也插入了内存屏障。我认为使用 Volatile
可以更好地传达代码的意图:
public ContactOptions Options
{
get { return Volatile.Read(ref _Options); }
set { Volatile.Write(ref _Options, value); }
}
- 在
get
或 set
访问器中省略同步会使您自动进入 memory models, cache coherency protocols and CPU architectures. In order to know if it's safe to omit it, intricate knowledge of the targeted hardware/OS configuration is required. You will need either an expert 建议的大黑森林,或者您自己成为专家。如果您更愿意停留在软件开发领域,请不要忽略同步!
我最近开始重新审视我的一些旧的多线程代码,想知道它们是否安全和正确(生产中还没有问题...)。特别是我是否正确处理了对象引用?我已经阅读了大量使用整数等简单基元的示例,但与引用和任何可能的细微差别有关的示例并不多。
首先,我最近了解到对象引用分配是原子的,至少在 64 位机器上是这样,这是我针对这个特定应用程序所关注的全部内容。以前,我锁定 class 属性的 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 的最新值时实现了获取 Options 的最新值的目标?使用此方法还有其他问题吗?
- 我相信锁有一些开销(转换为监视器。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 中的 lock/Interlock 用法将提供我正在寻找的同步。我已经尝试编写示例程序来强制执行陈旧的值场景,但我无法可靠地完成它。
private ContactOptions _Options;
public ContactOptions Options {
get { return Interlocked.CompareExchange(ref _Options, null, null); }
set { _Options = value; } }
旁注:
- ContactOptions class 故意不可变,因为我不想同步或担心选项本身的原子性。它们可能包含任何类型的数据,所以我认为在需要更改时分配一组新的选项是很多 cleaner/safer。
- 我熟悉获取值、使用该值然后设置该值的非原子含义。考虑以下片段:
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;
}
Correct me if I'm wrong here, but the above code does ensure that I'm achieving the goal of getting the latest value of Options when I get it in a thread safe manner? Any other issues using this method?
是的,锁会发出内存屏障,因此它会确保从内存中读取值。除了可能比必须的更保守之外,没有真正的问题。但是我有一句话,如果有疑问,就用一把锁。
I believe there is some overhead with the lock (Converts to Monitor.Enter/Exit). I thought I could use Interlocked for a nominal performance gain, but more importantly to me, a cleaner set of code. Would the following work to achieve synchronization?
Interlocked 也应该发出内存屏障,所以我认为这应该或多或少做同样的事情。
Since a reference assignment is atomic, is the synchronization (using either lock or Interlocked) necessary when assigning the reference? If I omit the set logic and only maintain the get, will I still maintain atomicity and synchronization? My hopeful thinking is that the lock/Interlock usage in the get would provide the synchronization I'm looking for. I've tried writing sample programs to force stale value scenarios, but I couldn't get it done reliably.
我认为在这种情况下,只要让字段变易变就足够了。据我了解,“过时值”的问题有些夸张,缓存一致性协议应该可以解决大多数问题。
据我所知,主要问题是阻止编译器只将值放入寄存器而不进行任何后续加载。并且 volatile
应该可以防止这种情况,迫使编译器在每次读取时发出加载。但这主要是在循环中重复检查值时出现的问题。
但只看一个属性用处不大。当您有多个需要同步的值时,问题会更频繁地出现。一个潜在的问题是编译器或处理器对指令重新排序。锁和内存屏障可以防止这种重新排序,但如果这是一个潜在的问题,最好锁定更大的代码段。
总的来说,我认为在处理多线程时偏执是谨慎的做法。使用更多的同步可能比使用更少的同步更好。一种例外情况是锁太多可能导致的死锁。我对此的建议是在持有锁时要非常小心你所调用的内容。理想情况下,锁应该只持有一个短的、可预测的时间。
还要继续使用纯函数和不可变数据结构。这些是避免担心线程问题的好方法。
- 是的,
lock (OptionsLock)
确保所有线程将看到Options
的最新值,因为 memory barriers 在进入和退出lock
时被插入。 - 将
lock
替换为Interlocked
或Volatile
class 的方法同样可以很好地实现最新值可见性目标。这些方法也插入了内存屏障。我认为使用Volatile
可以更好地传达代码的意图:
public ContactOptions Options
{
get { return Volatile.Read(ref _Options); }
set { Volatile.Write(ref _Options, value); }
}
- 在
get
或set
访问器中省略同步会使您自动进入 memory models, cache coherency protocols and CPU architectures. In order to know if it's safe to omit it, intricate knowledge of the targeted hardware/OS configuration is required. You will need either an expert 建议的大黑森林,或者您自己成为专家。如果您更愿意停留在软件开发领域,请不要忽略同步!