C# .NET:ImmutableList 引用副本周围是否需要“锁定”?

问题描述 投票:0回答:1

我们团队中就锁定声明进行了辩论 - 我希望得到一些有关此问题的外部反馈。

有问题的代码部分是:

lock (IndexLock)
{
    currentIndex = Index;
}

您可以在下面看到部分课程:

public class BuilderService
{
    private ImmutableList<Model> Index { get; set; } = ImmutableList<Model>.Empty;
    private object IndexLock { get; } = new();

    public Add(Model item)
    {
        lock (IndexLock)
        {
           Index = Index.Add(item);
        }
    }

    public IEnumerable<Model> GetFilterLinesExcluding()
    {
        var filters = new List<Model>();
        ImmutableList<FilterModel> currentIndex;
    
        lock (IndexLock)
        {
            currentIndex = Index;
        }
    
        foreach (var filter in currentIndex)
        {
            ...
        }        
    }
}

无需详细讨论,争论围绕以下现象:

  • 内存可视性
  • 记忆障碍
  • 陈旧的价值观

lock
周围有
currentIndex = Index
有意义吗?

主题:

  • GetFilterLinesExcluding
    多线程访问的方法。
  • Add
    方法可以随时调用。

如果需要,我会提供更多信息。

提前谢谢您!

c# reference locking
1个回答
0
投票

lock
周围有
currentIndex = Index
有意义吗?

是的。 使用

lock
语句是强制线程安全的最简单方法,无需进行大量研究。
lock
确保一次只有一个线程可以与共享状态交互,如果您小心且遵守纪律,则使多线程相对容易。当执行流进入和退出受保护的部分时,
lock
语句还隐式添加适当的内存屏障,因此您不必考虑它们。

我们团队就

lock
声明进行了辩论。

我猜您团队中的某人建议删除

lock
线周围的
currentIndex = Index;
。他们的论点很可能是
ImmutableList<T>
是不可变的,因此在枚举
ImmutableList<T>
时另一个线程不可能改变它。这是一个理性的论点,但无锁多线程却一点也不理性。您可能不知道,C# 编译器以及 .NET Jitter 和 CPU 处理器都可以对程序指令进行重新排序,前提是重新排序不会影响单个线程上的执行。换句话说,编译器/抖动/处理器假定您的程序是单线程的,除非您通过隐式或显式添加内存屏障来告诉它。因此,如果省略
lock
周围的
currentIndex = Index;
,则该行之后的指令可能会在此行之前执行。在这种特殊情况下,我看不到该行之后可以移动并放置在该行之前的任何内容,从而导致问题。因此,从技术上讲,当使用任何符合标准的 C# 编译器进行编译并在任何符合标准的 CPU 架构上运行时,省略
lock
可能不会改变程序的行为,但我不会把钱押在它上面。在走这条路之前,您需要征求内存模型和 CPU 架构方面真正“专家”的人的意见,而我不是那么专家。 如果您询问了

lock

线周围的

Index = Index.Add(item);
,我当然可以看到移除
that
锁的问题。然后线程就可以将未初始化的 ImmutableList<T> 对象分配给
Index
字段,然后对其进行初始化。从单线程程序的角度来看,这没有任何区别,因此这种重新排序是允许的。当然,其他线程可能会尝试枚举部分初始化的
ImmutableList<T>
实例,从而允许发生各种混乱。
顺便说一句,锁定的一种更高效且争议更少的替代方案是使用 

volatile

 关键字:
private volatile ImmutableList<Model> _index; // Not thread-safe. Only one thread is supposed to call it. public Add(Model item) { _index = _index.Add(item); } // Thread-safe. Multiple threads can call it. public IEnumerable<Model> GetFilterLinesExcluding() { List<Model> filters = new(); foreach (var filter in _index) { ... } }

每当读取或更新 
volatile

时,

_index
都会添加适当的屏障,从而防止在部分初始化状态下查看
ImmutableList<Model>
的情况。
如果您也想使 

Add

线程安全,而不使用

lock
语句,则可以使用
ImmutableInterlocked.Update
 API。这将防止可能导致某些 
Model
物品丢失的竞争条件。
    

© www.soinside.com 2019 - 2024. All rights reserved.