【问题标题】:Does using ReaderWriterLockSlim cause a memory barrier?使用 ReaderWriterLockSlim 会导致内存障碍吗?
【发布时间】:2008-12-20 15:32:33
【问题描述】:

如果我使用ReaderWriterLockSlim 来获取读/写锁,我需要将我的变量设为volatile 还是使用Interlocked.Increment

例如,下面Add 方法中的代码是否可以正常工作,还是需要增强?

public class AppendableList<T> { // semi-immutable; supports appending only
    private T[] data = new T[16];
    private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
    public int Count { get; private set; }
    public T this[int index] {
        get {
            rwLock.EnterReadLock();
            try { return data[index]; } finally { rwLock.ExitReadLock(); }
        }
    }
    public void Add(T item) {
        rwLock.EnterUpgradeableReadLock();
        try {
            if (Count == data.Length)
                reAllocateArray(); // upgrades to write lock
            data[Count++] = item; // do I need to use Interlocked here?
        } finally { rwLock.ExitUpgradeableReadLock(); }
    }
}

编辑:我正在尝试编写一个轻量级、快速且简单的列表,允许多个线程同时访问其数据(类似于生产者-消费者缓冲区)。我已经编辑了上面的代码,删除了我之前使用的简化,所以现在问题应该更清楚了。在我看来,这段代码是线程安全的,但我不确定是否所有线程都会在退出可升级锁后立即看到 Count 的更新值。

EDIT 2:这里的“Write”锁用于指示写入数组引用,而不是数组元素。我假设这已经足够了(因为数据本身是不可变的)。我想我需要在递增Count 时使用 Interlocked。这是真的吗?

【问题讨论】:

标签: .net multithreading locking


【解决方案1】:

我完全期望写锁起到内存屏障的作用(特别是在写锁内),但我无法立即证明这一点。

是否需要ReaderWriterLockSlim的复杂度取决于上下文; Interlockedvolatilelock[MethodImpl] 可能会更简单地完成工作。如果你的读者多,作者少,你主要需要ReaderWriterLock[Slim]

但是,get 当前不受锁保护;如果您需要通过写锁跨越多个操作(没有读者看到中间值),您将需要我们一个显式的属性实现并自己取出一个读锁。

顺便说一句,Count++ 的用法可能对人们来说更熟悉。

您还应该使用try/finally 来确保您释放异常锁定。

为了避免先写后读锁的问题,也许:

    private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();

    private int count;
    public int Count {
        get {
            rwLock.EnterReadLock();
            int tmp = count;
            rwLock.ExitReadLock();
            return tmp;
        }
    }
    public void Add(object x) {
        rwLock.EnterWriteLock();
        try {
            // do some processing
            count++;
        } finally {
            rwLock.ExitWriteLock();
        }
    }

更新了您的编辑;

这看起来很可靠。 List&lt;T&gt; 将是我的推荐(通过 T[] 数组),因为它会在内部完成所有加倍等操作,从而为您节省大量代码。由于一次只有一个线程可以更新Count,所以不需要Interlocked,而且这个属性省去了读取Count时对锁的需要,只要调用者获取旧的就好了Count 而另一个线程正在添加行(而不是被阻塞)。

【讨论】:

  • 您还可以考虑将计数标记为 volatile(与完全读锁相反),具体取决于您实际使用它的目的。如果您需要在看到新值之前 100% 确定写入已 100% 完成,请使用读锁。如果没关系,volatile 应该提供足够的保护。
  • 另一方面,如果您的属性比 int 更复杂,您将需要读锁; volatile 不会阻止您看到部​​分更新的复杂结构。
  • 注意:对于像你在这​​里暗示的集合,读锁可能是要走的路;但您应该逐案评估您真正需要多少保护。
  • 感谢马克和埃里克。我编辑了我的问题以澄清它。以前我已将示例简化为简洁,因此删除了 try/finally 和不相关的代码。我重写了示例以澄清我的问题。
  • 我假设代码是线程安全的,因为元素是不可变的,并且只有在有更多数据可用时才会增加计数。
【解决方案2】:

Yes 确实如此,对于各种内存屏障案例的深入概述,请查看该文档,如果您还想找到非围栏锁。

请,不要使用 VOLITILE,它现在越来越没效果了!!

所有标准 Windows 锁定 机制(自旋锁、互斥锁、 内核事件和资源管理 由执行资源包) 防止处理器重新排序 通过在哪里插入内存屏障 在可执行代码中是必需的。

内存屏障是处理器 指令保留 读和/或写的顺序 从任何角度的操作 其他处理器。记忆障碍 包括处理器指令 获取、释放和围栏语义。 这些语义描述了顺序 操作的结果变成 可见的。

  • 获取语义意味着操作的结果是可见的 在任何操作的结果之前 在代码中出现在它之后。
  • 释放语义意味着操作的结果是可见的 在任何操作的结果之后 在代码中出现在它之前。
  • Fence 语义结合了获取和释放语义。一个结果 具有围栏语义的操作是 在任何操作之前可见 出现在它之后的代码和 在任何操作之后 出现在它之前。

【讨论】:

    猜你喜欢
    • 2014-12-04
    • 2010-11-18
    • 2021-03-23
    • 2021-09-25
    • 2014-12-08
    • 1970-01-01
    • 1970-01-01
    • 2010-09-09
    相关资源
    最近更新 更多