锁辅助工具的线程安全使用(关于内存屏障)

5

所谓锁定助手,是指可以通过using语句实现锁定的一次性对象。例如,考虑使用Jon Skeet's MiscUtil中的SyncLock类的典型用法:

public class Example
{
    private readonly SyncLock _padlock;

    public Example()
    {
        _padlock = new SyncLock();
    }

    public void ConcurrentMethod()
    {
        using (_padlock.Lock())
        {
            // Now own the padlock - do concurrent stuff
        }
    }
}

现在,考虑以下用法:
var example = new Example();
new Thread(example.ConcurrentMethod).Start();

我的问题是这样的——由于example在一个线程上创建,而ConcurrentMethod在另一个线程上调用,那么ConcurrentMethod的线程是否可能对构造函数中_padlock的赋值毫不知情(由于线程缓存/读写重排序),从而抛出NullReferenceException(在_padLock本身上)?
我知道使用Monitor/lock进行锁定具有内存屏障的好处,但是使用此类锁辅助程序时,我看不到为什么可以保证这样的屏障。在这种情况下,据我所知,必须修改构造函数:
public Example()
{
    _padlock = new SyncLock();
    Thread.MemoryBarrier();
}

来源:了解低锁定技术在多线程应用中的影响

编辑 Hans Passant认为创建线程意味着存在内存屏障。那么如何处理:

var example = new Example();
ThreadPool.QueueUserWorkItem(s => example.ConcurrentMethod());

现在,线程不一定是在调用start()方法时就创建了...

1
你认为在什么时候可能会有一个缓存的“null”浮动存在? - Marc Gravell
除了Marc之外:_padLock引用不会改变,因此缓存是无关紧要的。第一次读取将在设置后发生。如果它是按需创建或其他什么的话,你的问题可能更有价值。 - H H
1
启动一个线程就足以强制更新缓存。你必须想出一个更好的例子。 - Hans Passant
@Marc,Henk - 构造函数的线程可以将同步锁分配到其缓存中,而不会显示在主内存中,这可能会被ConcurrentMethod的线程读取。 - Ohad Schneider
3
一样的。唤醒一个线程池线程仍然涉及到同步缓存的内部同步,任何线程上下文切换也是如此。 - Hans Passant
显示剩余6条评论
1个回答

11
不,你不需要做任何特殊的事情来确保创建内存屏障。这是因为几乎任何用于在另一个线程上执行方法的机制都会在调用线程上产生一个“释放屏障”屏障,并在工作线程上产生一个“获取屏障”屏障(实际上它们可能是完整的栅栏屏障)。因此,QueueUserWorkItemThread.Start将自动插入必要的屏障。您的代码是安全的。
此外,作为一个旁观者感兴趣的问题,Thread.Sleep也会生成一个内存屏障。这很有趣,因为有些人天真地使用Thread.Sleep来模拟线程交错。如果使用这种策略来排除低锁定代码问题,则很可能掩盖了您试图找到的问题。

4
"对于'Thread.Sleep也会生成内存屏障'这一说法给予+1,如果这确实是真的话,那非常有趣。" - Daniel Mošmondor
确实非常有趣。因此,在大多数情况下,在构造函数中我不需要内存屏障,因为创建对象的线程通常会在该对象上工作(或调度其他线程来处理它,这将产生内存屏障)。这样说对吗? - Ohad Schneider

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接