【问题标题】:How to guarantee that an update to "reference type" item in Array is visible to other threads?如何保证 Array 中“引用类型”项的更新对其他线程可见?
【发布时间】:2012-08-10 21:06:52
【问题描述】:
private InstrumentInfo[] instrumentInfos = new InstrumentInfo[Constants.MAX_INSTRUMENTS_NUMBER_IN_SYSTEM];

public void SetInstrumentInfo(Instrument instrument, InstrumentInfo info)
{
    if (instrument == null || info == null)
    {
        return;
    }
    instrumentInfos[instrument.Id] = info;  // need to make it visible to other threads!
}

public InstrumentInfo GetInstrumentInfo(Instrument instrument)
{
    return instrumentInfos[instrument.Id];  // need to obtain fresh value!
}

SetInstrumentInfoGetInstrumentInfo 是从不同的线程调用的。 InstrumentInfo 是不可变类。 拨打GetInstrumentInfo 时,我能保证获得最新的副本吗?恐怕我会收到“缓存”副本。我应该添加某种同步吗?

instrumentInfos 声明为volatile 没有帮助,因为我需要将数组项声明为volatile,而不是数组本身。

我的代码有问题吗?如果有,如何解决?

UPD1:

我需要我的代码在现实生活中工作,而不是符合所有规范!因此,如果我的代码在现实生活中有效,但在某些环境下的某些计算机上“理论上”不能工作 - 没关系!

  • 我需要我的代码在使用最新 .NET Framework 的 Windows 下在现代 X64 服务器(当前为 2 个处理器 HP DL360p Gen8)上工作。
  • 我不需要在奇怪的计算机或 Mono 或其他任何东西下处理我的代码
  • 我不想引入延迟,因为这是 HFT 软件。因此,由于“Microsoft 的实现使用强大的内存模型进行写入。这意味着写入被视为易失性”,我可能不需要添加额外的 Thread.MemoryBarrier,这只会增加延迟。我认为我们可以相信微软将在未来的版本中继续使用“强内存模型”。至少微软不太可能改变内存模型。所以我们假设它不会。

UPD2:

最近的建议是使用Thread.MemoryBarrier();。现在我不明白我必须将它插入的确切位置,才能使我的程序在 standard 配置(x64、Windows、Microsoft .NET 4.0)上运行。请记住,我不想插入“只是为了在 IA64 或 .NET 10.0 上启动您的程序”的行。速度对我来说比便携性更重要。然而,如何更新我的代码以便它可以在任何计算机上运行也会很有趣。

UPD3

.NET 4.5 解决方案:

    public void SetInstrumentInfo(Instrument instrument, InstrumentInfo info)
    {
        if (instrument == null || info == null)
        {
            return;
        }
        Volatile.Write(ref instrumentInfos[instrument.Id], info);
    }

    public InstrumentInfo GetInstrumentInfo(Instrument instrument)
    {
        InstrumentInfo result = Volatile.Read(ref instrumentInfos[instrument.Id]);
        return result;
    }

【问题讨论】:

  • @svick 所以我必须使用Thread.MemoryBarrier();?上面写的代码行不通?
  • 是的,这是一种选择。但如果可以的话,我更喜欢使用Thread.Volatile(Read|Write)(或Volatile.(Read|Write))。是的,我认为你的代码不能工作,因为它可以读取过时的数据。
  • 更糟。它在大多数情况下都会起作用,但有可能不起作用;取决于它在哪台机器上运行的潜力。不工作的代码比那更容易修复。
  • @JonHanna 这个代码可以在哪台机器上运行?如果它可以在 x64 机器上运行,那么我宁愿不介绍任何东西。因为我不需要延迟,因为它是 HFT 软件的一部分。
  • @Xantix 即 reference 分配保证在 C# 规范和 CLi 中都是线程安全的。

标签: c# multithreading synchronization volatile


【解决方案1】:

这是一个答案冗长而复杂的问题,但我会尝试将其提炼成一些可行的建议。

1.简单的解决方案:只在锁下访问instrumentInfos

在多线程程序中避免不可预测性的最简单方法是始终使用锁来保护共享状态。

根据您的 cmets,听起来您认为此解决方案过于昂贵。您可能需要仔细检查该假设,但如果确实如此,那么让我们看看剩下的选项。

2。高级解决方案:Thread.MemoryBarrier

或者,您可以使用 Thread.MemoryBarrier:

private InstrumentInfo[] instrumentInfos = new InstrumentInfo[Constants.MAX_INSTRUMENTS_NUMBER_IN_SYSTEM]; 

public void SetInstrumentInfo(Instrument instrument, InstrumentInfo info) 
{ 
    if (instrument == null || info == null) 
    { 
        return; 
    } 

    Thread.MemoryBarrier(); // Prevents an earlier write from getting reordered with the write below

    instrumentInfos[instrument.Id] = info;  // need to make it visible to other threads! 
} 

public InstrumentInfo GetInstrumentInfo(Instrument instrument) 
{ 
    InstrumentInfo info = instrumentInfos[instrument.Id];  // need to obtain fresh value! 
    Thread.MemoryBarrier(); // Prevents a later read from getting reordered with the read above
    return info;
}

使用 Thread.MemoryBarrier before 写入和 after 读取可以防止潜在的问题。第一个内存屏障防止写入线程重新排序初始化对象字段的写入,将对象发布到数组中,第二个内存屏障防止读取线程重新排序从数组接收对象的读取随后读取该对象的字段。

附带说明,.NET 4 还公开了使用 Thread.MemoryBarrier 的 Thread.VolatileRead 和 Thread.VolatileWrite,如上所示。但是,对于 System.Object 以外的引用类型,没有 Thread.VolatileRead 和 Thread.VolatileWrite 的重载。

3.高级解决方案(.NET 4.5):Volatile.Read 和 Volatile.Write

.NET 4.5 公开了比完全内存屏障更有效的 Volatile.Read 和 Volatile.Write 方法。如果您的目标是 .NET 4,则此选项无济于事。

4. “错误但会发生”解决方案

你永远不应该相信我要说的话。但是...您不太可能重现原始代码中存在的问题。

事实上,在 .NET 4 中的 X64 上,如果你能重现它,我会感到非常惊讶。 X86-X64 提供了相当强大的内存重新排序保证,因此这些发布模式恰好可以正常工作。 .NET 4 C# 编译器和 .NET 4 CLR JIT 编译器还避免了会破坏您的模式的优化。因此,允许对内存操作重新排序的三个组件都不会发生这种情况。

也就是说,发布模式的某些变体(有些晦涩难懂)实际上在 X64 中的 .NET 4 上不起作用。因此,即使您认为代码永远不需要在 .NET 4 X64 以外的任何体系结构上运行,如果您使用正确的方法之一,您的代码将更易于维护,即使该问题目前无法在您的服务器上重现.

【讨论】:

  • 如何防止计算机使用缓存值?我每次都需要将值写入主内存而不是缓存(因此另一个处理器可以使用它)。所以问题不在于“重新排序”......另一个问题:在 .NET 4.5 中我可以使用 Volatile.Read 数组项吗?
  • X64 使用MESI protocol 的变体来保持缓存的一致性。如果处理器修改其缓存中的值,则其他缓存中的值将失效。因此,缓存一致性在 X64 上不是问题。
  • 真正的问题是由于编译器中的“重新排序”(即优化)和指令在处理器管道中执行时的重新排序(在 X64 上非常有限)。
  • Volatile.Read 可以与数组一起使用。
  • 感谢伊戈尔。我决定使用 .NET 4.5 VolatileRead / VolatileWrite。我已将我的解决方案添加到我的问题描述中。如果您可以查看它,那就太好了!
【解决方案2】:

您可以使用系统级同步原语(如 Mutex);但那些有点笨拙。 Mutex 真的很慢,因为它是内核级别的原语。这意味着您可以拥有跨进程的互斥代码。这不是您需要的,您可以使用性能成本更低的东西,例如在单个进程中工作的 lockMonitor.EnterMonitor.Exit

您不能使用VolatileRead/WriteMemoryBarrier 之类的东西,因为如果数组中的元素未以原子方式分配,则您需要将写入集合的操作设为原子操作。 VolatileRead/WriteMemoryBarrier 不会这样做。这些只是给你获取和释放语义。这意味着写入的任何内容对其他线程都是“可见的”。如果您不对集合进行原子写入,则可能会损坏数据——获取/释放语义对此无济于事。

例如:

private InstrumentInfo[] instrumentInfos = new InstrumentInfo[Constants.MAX_INSTRUMENTS_NUMBER_IN_SYSTEM];

private readonly object locker = new object();

public void SetInstrumentInfo(Instrument instrument, InstrumentInfo info)
{
    if (instrument == null || info == null)
    {
        return;
    }
    lock(locker)
    {
        instrumentInfos[instrument.Id] = info;
    }
}

public InstrumentInfo GetInstrumentInfo(Instrument instrument)
{
    lock(locker)
    {
        return instrumentInfos[instrument.Id];
    }
}

并发集合也会这样做;但是,您会产生成本,因为 每个 操作都受到同步保护。另外,并发集合只知道对元素的访问的原子性,您仍然必须确保应用程序级别的原子性:如果您对并发集合执行以下操作:

public bool Contains(Instrument instrument)
{
   foreach(var element in instrumentInfos)
   {
       if(element == instrument) return true;
   }
}

...您至少会遇到几个问题。一,您不会阻止 SetInstrumentInfo 在枚举时修改集合(许多集合不支持此操作并抛出异常)。即集合仅在检索单个元素时“受保护”。第二,集合在每次迭代时都受到保护。如果您有 100 个元素并且并发集合使用了锁,那么您将获得 100 个锁(假设要查找的元素是最后一个或根本没有找到)。这将比它需要的慢得多。如果你不使用并发集合,你可以简单地使用 lock 来获得相同的结果:

public bool Contains(Instrument instrument)
{
   lock(locker)
   {
      foreach(var element in instrumentInfos)
      {
          if(element == instrument) return true;
      }
   }
}

拥有一个单一的锁并且性能更高。

UPDATE 我认为需要指出的是,如果InstrumentInfo 是一个结构,那么使用lock(或其他一些同步原语)就变得更加重要因为它将具有值语义,并且每个赋值都需要移动与InstrumentInfo 用于存储其字段一样多的字节(即,它不再是 32 位或 64 位本机字引用赋值)。即一个简单的InstrumentInfo 赋值(例如,一个数组元素)永远不会是原子的,因此不是线程安全的。

更新 2 如果替换数组元素的操作可能是原子的(如果 InstrumentInfo 是引用并且 IL 指令 stelem.ref 是原子的,则成为简单的引用赋值)。 (即,将元素写入数组的行为是原子的 wrt 我上面提到的)在这种情况下,如果处理instrumentInfos 的唯一代码是您发布的代码,那么您可以使用@987654341 @:

public void SetInstrumentInfo(Instrument instrument, InstrumentInfo info)
{
    if (instrument == null || info == null)
    {
        return;
    }
    Thread.MemoryBarrier();
    instrumentInfos[instrument.Id] = info;
}

public InstrumentInfo GetInstrumentInfo(Instrument instrument)
{
    var result = instrumentInfos[instrument.Id];
    Thread.MemoryBarrier();
    return result;
}

...如果可能的话,这相当于声明数组volatile 中的每个元素。

VolatileWrite 不起作用,因为它需要引用它要写入的变量,你不能给它一个数组元素的引用。

【讨论】:

  • +1,但将储物柜对象设为只读以防止任何人意外重新分配它。
  • @Xantix 有用点(更新代码),但它不会阻止字段更改值,它只是阻止分配给构造函数之外的字段。但是,这将阻止该字段更改,同时它可以用于代码中显示的任何锁......
  • 鉴于最初的问题似乎是使用全局静态值 - 可能从 MIDI 设备或类似设备读取 - 我认为直接锁定阵列本身是安全的。感谢您添加有关如何安全地执行“foreach”的说明!
  • @TedSpence 如果数组表示 所有 操作需要在它们之间是原子的,那么您可以锁定数组。如果您有多个使用数组的原子操作的原因,那么另一个对象(对象)是必要的。将另一个对象分配给锁定的开销非常低,这使得始终使用另一个对象变得更加容易。如果数组曾经在类外可见,则会引入潜在的死锁。我在帖子中没有看到任何关于静态值的内容...
  • @javapowered 这显然是您使用VolatileReadVolatileWrite 得到的顺序。关于为什么他们在哪里的一些细节:stackoverflow.com/questions/1773680/…
【解决方案3】:

解决此问题的首选方法是使用关键部分。 C# 有一个名为“Lock”的内置语言结构可以解决这个问题。 lock 语句确保同一时间在该临界区中不能有超过一个线程。

private InstrumentInfo[] instrumentInfos = new InstrumentInfo[Constants.MAX_INSTRUMENTS_NUMBER_IN_SYSTEM];

public void SetInstrumentInfo(Instrument instrument, InstrumentInfo info)
{
    if (instrument == null || info == null) {
        return;
    }
    lock (instrumentInfos) {
        instrumentInfos[instrument.Id] = info;
    }
}

public InstrumentInfo GetInstrumentInfo(Instrument instrument)
{
    lock (instrumentInfos) {
        return instrumentInfos[instrument.Id];
    }
}

这确实会影响性能,但它始终可以确保您获得可靠的结果。如果您曾经迭代过 instrumentInfos 对象,这将特别有用;对于这样的代码,您绝对需要 lock 语句。

请注意,lock 是一种通用解决方案,可确保任何复杂语句都以原子方式可靠地执行。在您的情况下,因为您正在设置一个对象指针,您可能会发现可以使用更简单的构造,如线程安全列表或数组 - 只要这两个函数是唯一接触过instrumentInfos 的函数。更多详情请点击此处:Are C# arrays thread safe?

编辑:关于您的代码的关键问题是:InstrumentInfo 是什么?如果它是从object 派生的,则每次更新共享数组时都需要注意始终构造一个新的InstrumentInfo 对象,因为在单独的线程上更新对象可能会导致问题。如果是structlock 语句将提供您需要的所有安全性,因为其中的值将在写入和读取时被复制。

编辑 2:通过更多研究,似乎确实存在某些情况下,线程共享变量的更改不会出现在某些编译器配置中。这个线程讨论了一个“bool”值可以在一个线程上设置并且永远不会在另一个线程上检索的情况:Can a C# thread really cache a value and ignore changes to that value on other threads?

然而,我注意到这种情况只存在于涉及 .NET 值类型的情况下。如果你看看乔·埃里克森的回应。这段特定代码的反编译说明了原因:线程共享的值被读入寄存器,然后再也不会重新读取,因为编译器优化了循环中不必要的读取。

鉴于您使用的是共享对象数组,并且您已经封装了所有访问器,我认为您使用原始方法是完全安全的。但是:使用锁定的性能损失非常小,几乎可以忘记。例如,我编写了一个简单的测试应用程序,尝试调用SetInstrumentInfoGetInstrumentInfo 1000 万次。性能结果如下:

  • 无锁定,1000 万设置和接听电话:2 秒 149 毫秒
  • 加锁,1000 万设置和接听电话:2 秒 195 毫秒

这转化为大致的原始性能:

  • 不加锁,每秒可以执行 4653327 次 get & set 调用。
  • 使用锁定,您可以每秒执行 4555808 次获取和设置调用。

在我看来,lock() 声明值得 2.1% 的性能折衷。以我的经验,getset 这样的方法只占用应用程序执行时间的最小部分。您可能最好在其他地方寻找潜在的优化,或者花费额外的钱来获得更高时钟频率的 CPU。

【讨论】:

  • lockMutex 是两个不同的东西。
  • 抱歉,我的写法非常不准确——我想的是“互斥”而不是“C# Mutex 类”。我会重写以使其更清楚。
  • 锁太贵了。如果可能,我想避免锁定,因为我正在编写 HFT。我可能会按照上面的建议使用Thread.MemoryBarrier();
  • 从文档来看,Thread.MemoryBarrier 可能不会影响您的代码:stackoverflow.com/questions/3556351/…。如果您只是关心最高可能的性能,您可以 A) 确保 InstrumentInfoobject,并且 B) 在没有 lock() 语句的情况下运行,并且 C) 确保修改共享数组的所有函数很简单。在这种情况下,您可以依赖对象引用是 64 位指针并且通常可能在单个原子指令中更新这一事实。
  • @TedSpence 我们正在谈论缓存。 “原子性”是不够的。为了防止缓存,我必须使用某种进程间同步。否则每个线程将使用它自己的“缓存”。
【解决方案4】:

据我多年的设备驱动程序编程所知,易失性用于可以在 CPU 控制之外更改的东西,即通过硬件干预,或用于映射到系统空间(CSR 等)的硬件内存.当线程更新内存位置时,CPU 会锁定缓存行并引发处理器间中断,以便其他 CPU 可以丢弃它。因此,您没有机会读取过时的数据。从理论上讲,如果数据不是原子的(多个四边形),您可能只担心并发写入和读取到相同的数组位置,因为读取器可能会读取部分更新的数据。我认为这不会发生在引用数组上。

我会大胆猜测您想要实现的目标,因为它看起来类似于我过去开发的应用程序+驱动程序,用于显示来自手机摄像头测试板的流式视频。视频显示应用程序可以对每一帧应用一些基本的图像处理(白平衡、偏移像素等)。这些设置是从 UI 中 SET 和从处理线程中获取的。从处理线程的角度来看,设置是不可变的。看起来您正试图用某种音频而不是视频来做类似的事情。

我在 C++ 应用程序中采用的方法是将当前设置复制到伴随“框架”的结构中。因此,每个帧都有自己的设置副本,将应用于它。 UI 线程执行锁定以将更改写入设置,处理线程执行锁定以复制设置。锁是必需的,因为多个四边形被移动了,并且没有锁我们可以确定读取部分更新的设置。这并不重要,因为在流式传输过程中可能没有人会注意到混乱的帧,但如果他们暂停视频或将帧保存到磁盘,那么他们肯定会在黑暗的墙壁中发现一个闪亮的绿色像素。就声音而言,即使在蒸制过程中也更容易发现故障。

那是案例一。我们来看案例二。

当设备被未知数量的线程使用时,如何对设备进行彻底的重新配置?如果你直接去做,那么你肯定会有几个线程从配置A开始,在这个过程中遇到配置B,这在很大程度上意味着死亡。这就是您需要诸如读写器锁之类的东西的地方。也就是说,一个同步原语将允许您等待当前活动完成,同时阻止新活动。这就是读写锁的本质。

案例二结束。现在让我们看看你有什么问题,如果我的猜测是对的。

如果您的工作线程在其处理周期开始时执行 GET 并在整个周期中保持该引用,那么您不需要锁,因为引用更新是原子的,正如 Peter 提到的。这是我建议给你的实现,相当于我在每帧处理开始时复制设置结构。

但是,如果您从整个代码中进行 多个 GET,那么您就有麻烦了,因为在同一个处理周期内,一些调用将返回引用 A,而一些调用将返回引用 B。如果没问题有了你的应用,你就是个幸运儿。

但是,如果您对此问题有疑问,则必须修复该错误或尝试通过在其之上构建来修补它。修复错误很简单:消除多个 GET,即使这需要您进行少量重写,以便您可以传递引用。

如果要修补错误,请使用 Reader-Writer 锁。每个线程获取一个读取器锁,执行一个 GET,然后保持锁,直到其循环完成。然后它释放读卡器锁。更新数组的线程获取写入器锁,执行 SET 并立即释放写入器锁。

该解决方案几乎可以工作,但是当代码长时间持有锁时,即使它是读取器锁,它也会变得很糟糕。

然而,即使我们忘记了多线程编程,还有一个更微妙的问题。一个线程在循环开始时获取读锁,在结束时释放它,然后开始一个新的循环并重新获取它。当写入器出现时,线程无法开始新的循环,直到写入器完成,这意味着直到所有其他正在工作的读取器线程都完成了它们的处理。这意味着一些线程在重新启动它们的周期之前会遇到意外的高延迟,因为即使它们是高优先级线程,它们也会被写入器锁的请求阻塞,直到所有当前读取器也完成它们的周期。如果您正在做音频或其他时间紧迫的事情,那么您可能希望避免这种行为。

我希望我猜对了,而且我的解释很清楚:-)

【讨论】:

  • volatile 用于赋予内存访问获取或释放语义。即使只有 CPU 在多核上下文中访问内存,这也是必要的。每个核心都可以在自己的缓存中缓存其他核心看不到的值。获取语​​义和释放语义意味着缓存的值将被刷新到物理内存,因此读取将看到对该内存的任何和所有写入。 volatile 也意味着编译器无法将变量优化到寄存器中,以供执行此类操作的编译器使用。
  • link 我引用“处理器间中断 (IPI) 是一种特殊类型的中断,一个处理器可以通过它中断多处理器系统中的另一个处理器。IPI 通常用于实现缓存COHERENCY 同步点”。经过 10 年的驱动程序编程和多线程 C/C++ 编程,我从未使用过易失性或内存屏障操作。显然,每个 CPU 无法看到其他 CPU 内部的内容,但如果它们碰巧在缓存中包含特定的缓存行,它可以向它们发送 IPI 以清除它们。
  • 基本上整个问题都是完全无效的。即使您使用锁进行读写,那又如何?如果线程 A 获得了锁并读取了值,然后线程 B 获得了锁并更新了值怎么办?线程 A 仍将继续使用“陈旧”对象,直到其工作完成。真正的问题是防止一些线程同时使用旧对象和一些新对象。需要的是 System.Threading.ReadWriterLock。
  • 不,lock 的开头是易失性读取,lock 的结尾是易失性写入,因此您可以获得获取和释放语义,以保证所有内核都能看到所有写入在lock 块末尾的lock 块内制作。
  • 当然,易失性写入保证了一些东西,包括编译器/操作系统将放置的障碍,但是(a)如果您正在写入原子数据并且(b)由于缓存一致性,您已经有了所需的保证) 这家伙需要一个 ReadWriterLock,线程在使用他们获得的对象时持有读取器锁,而线程在想要更新数组时请求写入器锁。那是他的问题不是易变的陈旧读数。首先,.NET 是一个讨论易失性和内存障碍的有趣环境。如果您需要 volatile 和 Barriers,那么您可能不是在 C# 中编程。
【解决方案5】:

由于某种我没有时间了解的原因,我无法在任何地方添加任何 cmets,所以我添加了另一个答案。对于给您带来的不便,我深表歉意。

UPD3 固然有效,但具有危险的误导性

如果实际需要 UPD3 解决方案,请考虑以下场景:ThreadA 在 CPU1 上运行并对 Var1 进行原子更新。然后 ThreadA 被抢占,调度器决定在 CPU2 上重新调度它。 Ooopss... 既然 Var1 在 CPU1 的缓存中,那么根据 UPD3 完全错误的逻辑,ThreadA 应该先对 Var1 进行 volatile 写入,然后再进行 volatile 读取。 Oooopsss... 一个线程永远不知道它什么时候被重新调度或者它最终会在哪个 CPU 上结束。因此,根据 UPD3 完全错误的逻辑,线程应该始终执行 volatile writes 和 volatile reads,否则它可能会读取过时的数据。这意味着所有线程都应该始终进行易失性读/写。

对于单线程,更新多字节结构(非原子更新)时问题会更严重。想象一下,有些更新发生在 CPU1 上,有些发生在 CPU2 上,有些发生在 CPU3 上,等等。

这有点让我想知道为什么世界还没有走到尽头。

【讨论】:

  • 我在同一个问题中提交了另一个解决方案。它不包含可复制粘贴的代码,并且包含大量文本。对不起,很长的文字,但主题很复杂。如果您阅读了它,但我写的东西仍然不清楚,那么我将非常乐意提供进一步的帮助。
  • @Dmitris Staikos 如果你说我的解决方案是“危险的误导”,你能提供或建议没有的解决方案吗?否则我必须使用“危险的误导”解决方案,因为没有其他解决方案。我会立即在下面阅读您的答案
【解决方案6】:

或者,您可以使用 System.Collections.Concurrent 命名空间中的一些类,例如 ConcurrentBag<T> 类。这些类被构建为线程安全的。

http://msdn.microsoft.com/en-us/library/system.collections.concurrent.aspx

【讨论】:

  • 显然它在内部使用了“用于同步”的东西。什么? lockMemoryBarrier?。我可以使用相同的。
【解决方案7】:

在这种情况下,您可以利用 ConcurrentDictionary。它是线程安全的。

【讨论】:

  • 显然它在内部使用了“用于同步”的东西。什么? lockMemoryBarrier?。我可以使用相同的。
  • @javapowered 我不知道 ConcurrentDictionary 的内部结构,但我知道 ConcurrentQueue。它使用整数数组来对应每个元素。它使用互锁的增量和减量。没有锁。内存屏障在某种意义上说,每个线程都在该数组中分配了一个插槽。您可以使用反射器检查更多。
【解决方案8】:

非常有趣的问题。我希望 Eric 或 Jon 进来澄清事实,但在那之前让我尽力而为。另外,让我在前面说我不是 100% 确定这个答案是正确的。

通常,在处理“我是否使该字段易变?”时问题,你担心这样的情况:

SomeClass foo = new SomeClass("foo");

void Thread1Method() {
    foo = new SomeClass("Bar");
    ....
}

void Thread2Method() {
    if (foo.Name == "foo") ...
    ...
    // Here, the old value of foo might be cached 
    // even if Thread1Method has already updated it.
    // Making foo volatile will fix that.
    if (foo.Name == "bar") ...
}

在您的情况下,您并没有更改整个数组的地址,而是更改了该数组中元素的值,该元素是(大概)本机字长的托管指针。这些更改是原子的,但我不确定它们是否保证具有内存屏障(根据 C# 规范获取/释放语义)并刷新数组本身。因此,我建议在数组本身上添加一个volatile

如果数组本身是易失的,那么我认为读取线程不可能从数组中缓存一个项目。您似乎不需要使元素变得易变,因为您没有在数组中就地修改元素 - 您正在用新元素彻底替换它们(至少从外观上看!)。

你可能会遇到麻烦的是:

var myTrumpet = new Instrument { Id = 5 };
var trumpetInfo = GetInstrumentInfo(myTrumpet);
trumpetInfo.Name = "Trumpet";
SetInstrumentInfo(myTrumpet, trumpetInfo);

在这里,您正在更新项目,所以看到缓存发生我不会感到非常惊讶。不过,我不确定它是否真的会。 MSIL 包含用于读取和写入数组元素的显式操作码,因此语义可能与标准堆读取和写入不同。

【讨论】:

  • 在数组本身上添加 volatile 只会使 instrumentInfos 的赋值和读取变得易失。如果他只分配该字段一次(这是他发布的内容),那么这将无助于线程安全,即在使数组上的元素替换或多或少线程安全方面无济于事。
  • 即生成的 stelem.ref 替换数组中的元素将不会使用 .volatile 操作码指定的内存地址。
  • 不,但是将加载哪个地址的问题仍然取决于数组中存储的地址,这受数组本身。我们都知道,如果他在原地更改 InstrumentInfo,它就不是线程安全的。问题实际上是“如果我将一个项目存储在数组中的位置 X,另一个线程会读取该项目还是 曾经位于位置 X 的项目?”
  • volatile 不会影响数组本身,只会影响变量。即用volatile 装饰变量不会给数组元素访问任何获取/释放语义。
  • 他没有改变 SetinstrumentInfo 中的字段 instrumentInfos,他改变了instrumentInfos 引用的内存。这是一个非常不同且重要的区别。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-29
  • 2016-08-26
  • 2018-10-01
  • 1970-01-01
相关资源
最近更新 更多