这个问题一直困扰着我:在多线程/多处理器环境中 - 是否有必要使用显式锁来同步对共享变量的访问?
这是一个场景:我有一个指向共享对象的全局变量。创建对象的实例时,对它的引用将放在变量中,并且可供其他线程/处理器访问。
对象本身是不可变的 - 一旦创建它就永远不会改变,因为多个线程可以访问它而无需任何额外的同步。
有一段时间我需要更新对象。我这样做是在侧面创建一个新的对象实例,然后在全局变量中放置对新对象的引用。
所以这就是问题:我可以考虑将引用替换为原子操作。换句话说,变量总是对我的对象有一个有效的引用 - 旧的或新的?
答案 0 :(得分:11)
是否有必要使用显式锁来同步对共享变量的访问?
必要吗?不是。一个好主意?是。低锁技术非常难以正确,很少有理由。请记住,有争议时锁定很慢;如果您的锁被争议,请修复导致它们被争议的问题,而不是采用低锁解决方案。
对象本身是不可变的 - 一旦创建它就永远不会改变,因为多个线程可以访问它而无需任何额外的同步。
真棒。
有一段时间我需要更新对象。我这样做是在侧面创建一个新的对象实例,然后在全局变量中放置对新对象的引用。
所以这就是问题:我可以考虑将引用替换为原子操作。换句话说,变量总是对我的对象有一个有效的引用 - 旧的或新的?
是。 C#规范保证对引用的操作是原子的。 然而原子性只是线程安全的一小部分。原子性只保证每次查看变量时,都会得到一个有效的引用。 并不保证每次查看变量时都会获得当前引用。它也不保证任何两个线程以相同的顺序看到相同的更改序列。它也不保证对两个不同变量的两个原子更新按照它们在每个变量上发生的顺序螺纹即可。 Atomicity几乎没有任何保证,它可能无法保证您的程序能够以您认为的方式运行。
我的建议:如果您可以避免在多个线程上访问此变量,请执行此操作。如果你无法避免它,请在它周围放锁。只有当你发现由于性能原因锁定太慢而且如果考虑采用危险的低锁技术(例如使变量易变,使用互锁交换等),你就无法消除足够的争用。
答案 1 :(得分:2)
更新引用是一种原子操作,因此您不需要在其周围使用锁定机制。请查看C# Language Specification,第12.5节。
说完这个之后,制作变量volatile
(第17.4.3节)可能是明智之举。这可确保编译器不会对语句重新排序以进行优化。如果您想了解volatile
的全部内容,请阅读Joseph Albahari的Threading in C#论文。
答案 2 :(得分:1)
The specification says对引用类型的变量赋值是原子的。因此,您对变量始终保持有效引用的假设是正确的。
但是,如果“偶尔”经常(对于某些经常的定义),那么您还应该创建变量volatile
。 Eric Lippert的优秀技术解释:What is the purpose of 'volatile' keyword in C#。
答案 3 :(得分:1)
在.NET中分配对象引用是原子的。这不太可能帮助你,一些线程可能在使用对象时多次加载该全局变量。使用它的一个版本,然后获得另一个版本不太可能达到目的。你不能再称它为不可变的了。或者就此而言,加载全局变量并在线程使用时变得陈旧。
如果您确实需要更新该对象引用,那么您需要制定一个锁定策略,以便所有线程都知道它。总是很难处理全局变量。
答案 4 :(得分:1)
你的计划应该适用于很多案件。
有一些值得注意的重要事项。永远不要在当前操作中多次引用全局变量。也就是说,代码永远不应该假设全局引用没有从一条指令更改为下一条指令。
internal static class EvilSingleton
{
public static int[] FavoriteNumbers;
}
internal class FavoriteNumberView
{
private ListBox selectNumberListBox;
private void LoadNumberListTheWrongWay()
{
Debug.Assert(selectNumberListBox != null);
selectNumberListBox.Clear();
if (EvilSingleton.FavoriteNumbers == null)
return;
// This code is just asking for null reference and array bounds exceptions!!!
for (int index = 0; index < EvilSingleton.FavoriteNumbers.Length; index++)
selectNumberListBox.Add(EvilSingleton.FavoriteNumbers[index].ToString());
}
private void LoadNumberListTheRightWay()
{
// Store the global reference in a local variable, and
// only use the local reference from now on.
int[] favoriteNumbers = EvilSingleton.FavoriteNumbers;
Debug.Assert(selectNumberListBox != null);
selectNumberListBox.Clear();
if (favoriteNumbers == null)
return;
// The local reference will never be null, and the array bounds will always be
// correct. In fact, the optimizer can remove most bounds checking from this
// loop, since it knows the array length will not change.
for (int index = 0; index < favoriteNumbers.Length; index++)
selectNumberListBox.Add(favoriteNumbers[index].ToString());
}
}
答案 5 :(得分:0)
不,你不能这么认为。你应该在对象周围有一些锁定机制 - 在这种情况下是一个ReaderWriter锁(或ReaderWriterLockSlim) - 任何读取访问,使用读取器锁。任何时候你要交换它,使用编写器锁。
感谢。你是对的,应该检查规格。
答案 6 :(得分:0)
看看这篇文章:http://iridescence.no/post/The-Atomicity-of-Variable-References.aspx
CLR确保引用变量的赋值是原子操作。
在SO上也可以看到Eric Lippert对this question的回答。
答案 7 :(得分:0)
您使用的是.NET 4.0吗?如果是这样,您可能需要查看System.Collections.Concurrent Namespace。如果您不需要有序列表,则可以使用ConcurrentBag。