我试图理解对字段的线程安全访问。为此,我实施了一些测试样本:
class Program
{
public static void Main()
{
Foo test = new Foo();
bool temp;
new Thread(() => { test.Loop = false; }).Start();
do
{
temp = test.Loop;
}
while (temp == true);
}
}
class Foo
{
public bool Loop = true;
}
正如预期的那样,有时候它并没有终止。我知道可以使用volatile关键字或使用lock来解决此问题。我认为我不是Foo类的作者,所以我不能让字段变得不稳定。我尝试使用锁:
public static void Main()
{
Foo test = new Foo();
object locker = new Object();
bool temp;
new Thread(() => { test.Loop = false; }).Start();
do
{
lock (locker)
{
temp = test.Loop;
}
}
while (temp == true);
}
这似乎解决了这个问题。只是为了确保我将循环移动到锁定块内:
lock(locker)
{
do
{
temp = test.Loop;
}
while (temp == true);
}
和...程序不再终止。
这让我很困惑。 不锁定提供线程安全访问吗?如果没有,如何安全地访问非易失性字段?我可以使用VolatileRead(),但它不适用于任何情况,如非原始类型或属性。 我认为Monitor.Enter完成了这项工作,我是对的吗?我不明白它是如何运作的。
答案 0 :(得分:4)
这段代码:
do
{
lock (locker)
{
temp = test.Loop;
}
}
while (temp == true);
因lock
的副作用而起作用:它会导致'memory-fence'。实际锁定在这里无关紧要。等效代码:
do
{
Thread.MemoryBarrier();
temp = test.Loop;
}
while (temp == true);
你在这里试图解决的问题并不完全是线程安全的,而是关于缓存变量(陈旧数据)。
答案 1 :(得分:2)
它不再终止,因为你也在锁外部访问变量。
在
new Thread(() => { test.Loop = false; }).Start();
你写入锁外的变量。此写入不保证可见。
对同一位置的两次并发访问(其中至少一次是写入)是数据争用。不要那样做。
答案 2 :(得分:0)
Lock为使用锁的不同线程上的2个或更多代码块提供线程安全性。 新线程声明中的循环赋值未包含在锁定中。 这意味着那里没有线程安全。
答案 3 :(得分:0)
一般来说,不,lock
不会神奇地使其中的所有代码都是线程安全的。
简单的规则是:如果你有一些由多个线程共享的数据,但你总是只在一个锁(使用相同的锁对象)内访问它,那么该访问是线程安全的。
一旦你离开那个“简单”的代码并开始提问“我怎么能在这里安全地使用volatile
/ VolatileRed()
?”或“为什么这段代码没有使用锁定正确似乎工作?“,事情变得很复杂。除非你准备花很多时间学习C#内存模型,否则你应该避免这种情况。即便如此,只有一百万次运行或仅在某些CPU(ARM)上出现的错误很容易制作。
答案 4 :(得分:0)
锁定仅在通过锁控制对该字段的所有访问时起作用。在您的示例中,只有读取被锁定,但由于写入不是,因此没有线程安全性。
然而,锁定发生在共享对象上也是至关重要的,否则其他线程无法知道某人正在尝试访问该字段。因此,在您锁定一个仅限在Main方法范围内的对象的情况下,另一个线程上的任何其他调用都无法阻止。
如果你无法改变Foo,获得线程安全的唯一方法是让所有调用实际锁定在同一个Foo实例上。但通常不建议这样做,因为对象上的所有方法都将被锁定。
volatile关键字本身并不是线程安全的保证。它意味着可以指示字段的值可以从不同的线程更改,因此读取该字段的任何线程都不应该缓存它,因为值可能会更改。
为了实现线程安全,Foo应该看起来像这样:
class Program
{
public static void Main()
{
Foo test = new Foo();
test.Run();
new Thread(() => { test.Loop = false; }).Start();
do
{
temp = test.Loop;
}
while (temp == true);
}
}
class Foo
{
private volatile bool _loop = true;
private object _syncRoot = new object();
public bool Loop
{
// All access to the Loop value, is controlled by a lock on an instance-scoped object. I.e. when one thread accesses the value, all other threads are blocked.
get { lock(_syncRoot) return _loop; }
set { lock(_syncRoot) _loop = value; }
}
public void Run()
{
Task(() =>
{
while(_loop) // _loop is volatile, so value is not cached
{
// Do something
}
});
}
}