【问题标题】:C# Garbage Collector, Threading and Compiler/Jitter optimizationC# 垃圾收集器、线程和编译器/抖动优化
【发布时间】:2015-08-05 11:45:13
【问题描述】:

假设我们的程序有一个中心点(Document 类的一个实例),这里引用了各种信息。 现在我们有两个线程。两个线程都可以访问我们的“文档”,并且“文档”包含对“参数”(一个包含某种信息的对象)的引用。因此,如果我们有对“document”的引用,我们可以使用“document.params”来获取我们的 params 对象。

线程 1 执行以下操作:

Params tempParams = document.params; // get a local reference to documents.params
int a = tempParams.a; // read data from params
// thread 1 (this thread) gets interrupted by thread 2
int b = tempParams.b; // read data from params
int c = tempParams.c; // read data from params

线程 2 执行以下操作:

Params newParams = new Params();
... // fill newParams with new parameters
lock(obj) {
    document.params = newParams; // update params in document
}

因此“params”的内容永远不会改变,但如果需要更改,则会生成一个新副本,并将引用“document.params”更新为新的 Params 块,这是一个原子操作。

现在最大的问题是:

抖动是否有可能优化线程 1 的代码,使 tempParams 不是内存地址而是 CPU 寄存器?如果线程 2 更新了 document.params 引用,内存中没有指向旧的“Params”块的引用,因为线程 1 中的引用仅在 CPU 寄存器中。如果此时垃圾收集器启动,它怎么能看到旧的“Params”块仍在使用中?

另一个问题是:是否会发生抖动优化掉 tempParams 变量并直接使用 document.params.a/b/c 的情况。在这种情况下,线程 1 会看到不打算交换的 Params 对象。使用 tempParams 应确保线程 1 从复制引用时 document.params 中的同一 Params 对象访问 a/b/c。

【问题讨论】:

  • 可能发生的最糟糕的情况是线程 1 下一次仍会读取 old document.params,即使您同时从线程 2 更改了它。这就是为什么您要在每次访问共享字段时使用锁或内存屏障。或者完全避免使用共享字段:D
  • 刚刚更新了更新文档的params字段的锁。线程 1 中丢失的锁是有意的,在线程 1 看到线程 2 所做的更改之前可能会有延迟。
  • 编写在调试时保证有错误的程序几乎没有意义。
  • 只要您对最终的一致性感到满意,它可能是安全的。不过,请确保您不依赖于该锁之外的操作顺序(例如,设置一个标志以及赋值或其他东西)。从理论上讲,线程 1 可能永远看到更新的值,但在实际代码中这可能不是问题。不过,在 C#/JIT/CLR 的未来版本中,这种事情可能会发生。
  • @Hans Passant:您究竟在哪里看到了错误?它正在工作。但这可能是偶然的,而不是正确的。

标签: c# multithreading reference garbage-collection


【解决方案1】:

抖动是否有可能优化线程 1 的代码,使 tempParams 不是内存地址而是 CPU 寄存器?

我怀疑这是可能的——但这不会阻止垃圾收集器将其视为对引用的使用。如果是这样,那将是一个 GC 错误。

另一个问题是:是否会发生抖动优化掉 tempParams 变量并直接使用 document.params.a/b/c 的情况。

那将是一个 JIT 错误,IMO。无法保证线程 1 会看到对 document.params 的更改(因此在不同情况下仍然需要考虑风险),但鉴于它将引用复制到局部变量中 (tempParams ) 并且该变量永远不会改变它的值,所有通过tempParams 的访问都指向同一个对象。 (tempParams.a 从一个对象读取但tempParams.b 从另一个对象读取是没有风险的。)

只是将下面的一些评论带入这个答案 - 有一些讨论围绕 JIT “优化”代码是否有效,以使其似乎改变了局部变量的值。例如,This MSDN article 当然表明它是有效的。我看到something similar 并在博客上写了a long time ago。我有 99% 的把握与某人(可能是 Joe Duffy)就 ECMA-335 有效的阅读介绍是否有效进行了交谈,他们的印象是事实并非如此。但是,我找不到任何明确的文档,ECMA-335至少在这件事上还不清楚。

ECMA-335 (CLI) 规范肯定比 MS 已经实施了一段时间的 CLR 2.0 模型更宽松,但我不认为它很宽松。如果您不能依赖与更改隔离的局部变量,则很难编写任何有效代码,IMO。

【讨论】:

  • 1) 根据这个 MSDN (msdn.microsoft.com/en-US/us-en/library/…) 没有计数器。同样,例如levibotelho.com/development/how-does-the-garbage-collector-work。 --- 2)有没有办法强制jitter做一个本地副本而不优化掉?
  • @bebo:我不建议那里有 柜台。我说过垃圾收集仍会将其视为对引用的使用 - 即它仍会注意到它正在使用中。我将更新答案以说“将其视为引用的使用”以避免歧义。基本上你不需要担心它会被提前 GC。对于 2) 我的观点是它制作本地副本 - 或者至少表现得好像它已经这样做了。
  • 对于线程,编译器会将params值复制到临时存储中(无论优化过程是什么),它会在线程终止时或指令“释放”用于GC的document.Params temparams=null" 被执行。您不必担心 Thread2 所做的更改,除非这些更改是在 thread1 开始执行之前进行的。
  • @bebo:据记录,当您的代码仍在使用该对象时,该对象不会被垃圾回收......而且您显然 正在 仍在使用它,所以这就是第一部分。至于第二部分,您基本上是在询问是否可以观察到局部变量以无根据地更改其值...我不知道它是否明确记录了 不会发生 ,但这似乎是一个合理的假设......
  • 对于第二个问题,我认为可以从一个对象读取 a 并从另一个对象读取 b。 Joe duffy 在他的书[Windows 上的并发编程] 中说,优化器(编译器/JIT/处理器或其他)更改程序以使程序的含义在由单线程执行时不会改变是完全合法的。因此,这与您在答案的第二部分中所说的相矛盾。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-10-15
  • 2013-04-01
  • 2011-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多