在我连续执行两个语句时,读/写一个布尔值是否需要进行锁定?

5

我想知道这段代码是否线程安全,如果不是,我该如何使其线程安全。这段代码被一个定时器调用:

private volatile bool _isSynchronizing;

private void SynchronizeSessionCache(object state = null)
{
    if (_isSynchronizing)
    {
        Log.Warn($"Aborted synchronization of SessionCache with SessionManager because we are already synchronizing. Interval is: {SynchronizationInterval}");
        return;
    }

     _isSynchronizing = true;

    bool lockWasTaken = false;
    try
    {
        // some code  that doesn't need a lock ...
        // ...

        // lock this part
        Monitor.Enter(_lockObject, ref lockWasTaken);
        // main code ...
    } 
    finally  // omitted catch
    {
        _isSynchronizing = false; 
        if(lockWasTaken)
            Monitor.Exit(_lockObject);
    }
}

我的担忧是,一个线程在方法开头检查_isSynchronizing,此时它是false。然后另一个线程进入该方法体,因为它还没有被线程1设置为true。即使_isSynchronizingvolatile,这种情况是否可能?如果是,最好的方法是什么,以使该方法线程安全?
如果我理解正确,volatile不能防止这种竞态条件,但只能确保变量不会被缓存,因此所有线程始终读取当前值。

是的,两个线程都能通过第一个检查。但是,在方法内部你有一个锁,所以你期望多个线程进入,那么这是什么意思呢? - Evk
@Evk:我不希望多个线程进入。_lockObject 在其他需要访问共享资源的方法和事件中也被使用。在同步期间必须锁定它。绝不能发生多个线程同时进入的情况。这就是为什么我使用 _isSynchronizing 标志的原因。 - Tim Schmelter
那么我会在这里使用双重检查锁定。 - Evk
4
_isSynchronizing 改为 int 类型,然后编写 if (Interlocked.CompareExchange(ref _isSynchronizing, 1, 0) != 0) 代码。这样就可以解决问题而不需要使用锁。 - Lucas Trzesniewski
在C#中,volatile确保始终读取最新值的想法是错误的,因此请停止相信它。 C#规范仔细描述了volatile的实际含义,即对写入volatile变量的影响如何与其他读取和写入重新排序存在限制。正如我多次所说,您应该阅读此内容以查看是否理解了volatile访问的重新排序方式:http://blog.coverity.com/2014/03/26/reordering-optimizations/ - Eric Lippert
特别是,“最新值”这个概念意味着一个变量在任何时刻都有一致可观察的值。这种假设是错误的;C#规范指出,如果没有锁,不一定存在所有线程从易失性写入中观察到一致的排序。即使在一切都是易失性的世界里,线程也可能就突变发生的顺序而产生分歧。 - Eric Lippert
1个回答

4
为确保线程安全性,您需要将比较和赋值操作合并为单个原子操作。否则,第一个线程执行赋值操作时,另一个线程始终有机会通过比较操作。在这里,关键字volatile并没有起到帮助作用,因为它只告诉编译器(和运行时),不允许对此变量所涉及的读写操作进行任何可能改变序列的优化。
(更多有关volatile的信息可以阅读Eric Lippert的这篇精彩文章
简单(稍慢)的解决方案是在比较和赋值之间设置临界区:
lock (_someLockObject)
{
    if (_isSynchronizing)
    {
         return;
    }

   _isSynchronizing = true;    
}

然而,针对 bool 变量并无更快的解决方案。对于一个 int 变量,您可以使用 Interlocked.CompareExchange() 方法。

假设 int _isSynchronizing = 0 表示 false_isSynchronizing = 1 表示 true

那么你可以使用以下语句:

if (Interlocked.CompareExchange(ref _isSynchronizing, 1, 0) == 1)
{
    // If the original val == 1, we're already synchronizing
    return;
}

这比使用Monitor略快,但没有bool重载。


为了在这种情况下保持清晰。使用Interlocked.Decrement(ref _isSynchronizing);或直接赋值为零而不使用锁等,哪个是正确的? - NevilleDastur

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