【问题标题】:Does an EventWaitHandle have any implicit MemoryBarrier?EventWaitHandle 是否有任何隐式 MemoryBarrier?
【发布时间】:2009-03-25 13:09:26
【问题描述】:

刚接触这个网站,所以如果我没有以可接受的方式发帖,请告诉我。

我经常按照下面的示例编写一些代码(为了清楚起见,省略了 Dispose 之类的内容。)。我的问题是,是否需要如图所示的挥发物?或者 ManualResetEvent.Set 是否像我读过的那样具有隐式内存屏障 Thread.Start 呢?或者明确的 MemoryBarrier 调用会比 volatile 更好吗?还是完全错误?此外,据我所见,某些操作中的“隐式内存屏障行为”没有记录,这很令人沮丧,在某处是否有这些操作的列表?

谢谢, 汤姆

class OneUseBackgroundOp
{

   // background args
   private string _x;
   private object _y;
   private long _z;

   // background results
   private volatile DateTime _a
   private volatile double _b;
   private volatile object _c;

   // thread control
   private Thread _task;
   private ManualResetEvent _completedSignal;
   private volatile bool _completed;

   public bool DoSomething(string x, object y, long z, int initialWaitMs)
   {
      bool doneWithinWait;

      _x = x;
      _y = y;
      _z = z;

      _completedSignal = new ManualResetEvent(false);

      _task = new Thread(new ThreadStart(Task));
      _task.IsBackground = true;
      _task.Start()

      doneWithinWait = _completedSignal.WaitOne(initialWaitMs);

      return doneWithinWait;

   }

   public bool Completed
   {
      get
      {
         return _completed;
      }
   }

   /* public getters for the result fields go here, with an exception
      thrown if _completed is not true; */

   private void Task()
   {
      // args x, y, and z are written once, before the Thread.Start
      //    implicit memory barrier so they may be accessed freely.

      // possibly long-running work goes here

      // with the work completed, assign the result fields _a, _b, _c here

      _completed = true;
      _completedSignal.Set();

   }

}

【问题讨论】:

    标签: c# .net vb.net multithreading volatile


    【解决方案1】:

    请注意,这是即兴的,无需仔细研究您的代码。我不认为 Set 执行内存屏障,但我看不出这与您的代码有什么关系?似乎更重要的是 Wait 执行一个,它确实执行了。因此,除非我在专门查看您的代码的 10 秒内错过了什么,否则我认为您不需要 volatile。

    编辑:评论过于严格。我现在指的是马特的编辑。

    马特的评估做得很好,但他遗漏了一个细节。首先,让我们提供一些被抛出的东西的定义,但这里没有澄清。

    易失性读取读取一个值,然后使 CPU 缓存无效。易失性写入会刷新缓存,然后写入值。内存屏障会刷新缓存,然后使其无效。

    .NET 内存模型确保所有写入都是易失的。默认情况下,不会读取,除非进行了显式 VolatileRead,或者在字段上指定了 volatile 关键字。此外,interlocked 方法强制缓存一致性,所有同步概念(Monitor、ReaderWriterLock、Mutex、Semaphore、AutoResetEvent、ManualResetEvent 等)在内部调用 interlocked 方法,从而确保缓存一致性。

    同样,所有这些都来自 Jeffrey Richter 的著作《CLR via C#》。

    我最初说过,我没有认为 Set 执行了内存屏障。然而,进一步思考 Richter 先生所说的话,Set 将执行一个联锁操作,因此也将确保缓存的一致性。

    我坚持我最初的断言,即此处不需要 volatile。

    编辑 2:看起来您正在构建一个“未来”。我建议您查看PFX,而不是自己滚动。

    【讨论】:

    • 请解释易失性与内存可见性的关系。
    • 根据 Jeffrey Richter 的“CLR via C#”,在 CLR 中,volatile 提供内存可见性语义。 volatile 数据的读取是无序的,而写入使用的是 release 语义。
    • Richter 的书(第 628 页)说,在 IA64 上,“所有写入始终使用释放语义执行,但允许无序读取;程序员仍必须调用 Thread 的 VolatileRead 或应用 C# volatile字段的关键字,以便通过获取语义读取它。”
    • 在第 626 和 627 页上,他描述了 volatile 的操作,这一描述与我的评论一致,即 volatile 在 C# 中的工作方式与在 C++ 中的工作方式相同。
    • ManualResetEvent 是线程同步对象,不是数据访问同步对象。请参阅我原始帖子的编辑。我不是想在这里开个楔子。如果我错了,我会非常乐意修改我的言论,以确保我所说的准确无误。
    【解决方案2】:

    在使 _a、_b 和 _c 线程安全时,不应混淆 volatile 关键字。请参阅here 以获得更好的解释。此外,ManualResetEvent 对 _a、_b 和 _c 的线程安全没有任何影响。您必须单独管理。

    编辑:通过此编辑,我试图提炼已在有关此问题的各种答案和 cmets 中提供的所有信息。

    基本问题是结果变量(_a、_b 和 _c)在标志变量 (_completed) 返回 true 时是否“可见”。

    暂时,让我们假设没有一个变量被标记为 volatile。在这种情况下,结果变量可以在 在 Task() 中设置标志变量之后设置,如下所示:

       private void Task()
       {
          // possibly long-running work goes here
          _completed = true;
          _a = result1;
          _b = result2;
          _c = result3;
          _completedSignal.Set();
       }
    

    这显然不是我们想要的,那么我们该如何处理呢?

    如果这些变量被标记为 volatile,那么这种重新排序将被阻止。但这就是引发原始问题的原因 - 是必需的 volatile 还是 ManualResetEvent 是否提供了隐式内存屏障以便不会发生重新排序,在这种情况下 volatile 关键字并不是真正必要的?

    如果我理解正确,wekempf 的立场是 WaitOne() 函数提供了一个隐式内存屏障来解决问题。 但是这对我来说似乎还不够。主线程和后台线程可以在两个单独的处理器上执行。因此,如果 Set() 也没有提供隐式内存屏障,那么 Task() 函数最终可能会在其中一个处理器上像这样执行(即使使用 volatile 变量):

       private void Task()
       {
          // possibly long-running work goes here
          _completedSignal.Set();
          _a = result1;
          _b = result2;
          _c = result3;
          _completed = true;
       }
    

    我搜索了有关内存屏障和 EventWaitHandles 的信息,但一无所获。我看到的唯一参考资料是 wekempf 对 Jeffrey Richter 的书所做的参考资料。我遇到的问题是 EventWaitHandle 旨在同步线程,而不是访问数据。我从未见过任何使用 EventWaitHandle(例如 ManualResetEvent)来同步访问数据的示例。因此,我很难相信 EventWaitHandle 在内存屏障方面做了任何事情。否则,我希望在互联网上找到一些对此的参考。

    编辑#2:这是对 wekempf 对我的回应的回应...... ;)

    我设法在 amazon.com 上阅读了 Jeffrey Richter 书中的部分。从第 628 页开始(wekempf 也引用了这一点):

    最后,我应该指出,每当线程调用互锁方法时,CPU 都会强制缓存一致性。因此,如果您通过互锁方法操作变量,则不必担心所有这些内存模型的东西。此外,所有线程同步锁(MonitorReaderWriterLockMutexSemaphoneAutoResetEvent >、ManualResetEvent 等)在内部调用联锁方法。

    因此,正如 wekempf 所指出的,结果变量确实需要示例中的 volatile 关键字,因为 ManualResetEvent 确保了缓存的一致性。

    在结束本次编辑之前,我还想补充两点。

    首先,我最初的假设是后台线程可能会运行多次。我显然忽略了类的名称(OneUseBackgroundOp)!鉴于它只运行一次,我不清楚为什么 DoSomething() 函数会以这种方式调用 WaitOne() 。如果后台线程在 DoSomething() 返回时可能完成也可能不完成,那么等待 initialWaitMs 毫秒有什么意义?为什么不直接启动后台线程并使用锁来同步对结果变量的访问 OR 只需将 Task() 函数的内容作为调用线程的一部分执行做一点事()?有理由不这样做吗?

    其次,在我看来,不对结果变量使用某种锁定机制仍然是一种不好的方法。诚然,代码中不需要它,如图所示。但是在某个时候,另一个线程可能会出现并尝试访问数据。在我看来,最好现在就为这种可能性做好准备,而不是以后试图追查神秘的行为异常。

    感谢大家对我的支持。通过参与这次讨论,我当然学到了很多东西。

    【讨论】:

    • 就像我回复 Jim Mischel 的评论一样,这里讨论的是内存可见性,其中 volatile 非常相关。
    • 在提供的链接中,我的示例符合“脚注:易失性实际上有什么好处”。因为您有 1 个作者,随后有 1 个读者,EventWaitHandle 可以防止“早期”访问结果。 Volatile 只是为了避免内存缓存问题(陈旧值)
    • 不,您的样本似乎不适合,正如我的回答所示。等待操作可确保您没有内存缓存问题。脚注建议一种不需要“锁定”的更快解决方案(在这方面,等待是“锁定”)。
    • 也许我理解错了,但是 Wait 操作如何确保没有与 volatile 字段相关的内存缓存?您是在说 ManualResetEvent 本身吗?
    • 从“CLR via C#”开始,所有线程同步锁(本例中为Wait)确保内存可见性。
    【解决方案3】:

    等待函数有一个隐含的内存屏障。见http://msdn.microsoft.com/en-us/library/ms686355(v=vs.85).aspx

    【讨论】:

      【解决方案4】:

      首先,我不确定是否应该“回答我自己的问题”或为此使用评论,但这里是:

      我的理解是 volatile 会阻止代码/内存优化移动对我的结果变量(和完成的布尔值)的访问,以便读取结果的线程将看到最新数据。

      由于编译器或 emmpry 优化/重新排序,您不希望 _completed 布尔值在 Set() 之后 对所有线程可见。同样,您不希望在 Set() 之后看到对结果 _a、_b、_c 的写入。

      编辑:关于问题的进一步解释/澄清,关于马特戴维斯提到的项目:

      最后,我要指出 每当一个线程调用一个互锁的 方法,CPU强制缓存 连贯性。所以如果你在操纵 通过联锁方法变量,你 不必担心这一切 记忆模型的东西。此外,所有 线程同步锁(监视器, ReaderWriterLock, Mutex, Semaphone, 自动重置事件,手动重置事件, 等)调用联锁方法 内部。

      所以看起来,就像 wekempf 指出,结果变量 不需要 volatile 关键字 如图所示的例子,因为 ManualResetEvent 确保缓存 连贯性。

      所以你们都同意这样的操作负责处理器之间或寄存器等的缓存。

      但是它是否会阻止重新排序以保证结果都被分配完成标志之前,并且完成标志被分配为真在手动重置事件设置之前

      首先,我最初的假设是 后台线程会 可能运行多次。一世 明显忽略了名字 类(OneUseBackgroundOp)!鉴于 它只运行一次,不清楚 对我来说为什么 DoSomething() 函数 以它的方式调用 WaitOne() 做。等待的意义是什么 initialWaitMs 毫秒,如果 后台线程可能是也可能不是 当时做的 DoSomething() 回报?为什么不直接启动 后台线程并使用锁 同步访问结果 变量或简单地执行 Task() 函数的内容为 调用的线程的一部分 做一点事()?有没有理由不 这样做?

      示例的概念是执行可能长时间运行的任务。如果任务可以在一个可排除的时间内完成,那么调用线程将获得结果并继续正常处理。但是有时一个任务可能需要相当长的时间,并且在此期间不能阻塞调用线程,并且可以采取合理的步骤来处理它。这可以包括稍后使用 Completed 属性检查操作。

      一个具体的例子:DNS 解析通常非常快(亚秒级),即使从 GUI 也值得等待,但有时可能需要很多秒。因此,通过使用像示例这样的实用程序类,可以在 95% 的时间里从调用者的角度轻松获得结果,而在另外 5% 的时间里不会锁定 GUI。可以使用后台工作人员,但这对于大多数时间不需要所有管道的操作来说可能是多余的。

      第二,在我看来,不使用 某种锁定机制 结果变量仍然很糟糕 方法。确实,在 代码如图所示。

      结果(和完成标志)数据是一次写入,多次读取。如果我添加了一个锁来分配结果和标志,我还必须锁定我的结果 getter,而且我从不喜欢看到 getter 锁定只是为了返回一个数据点。从我的阅读来看,这种细粒度的锁定是不合适的。如果一个操作有 5 或 6 个结果,调用者必须不必要地获取和释放锁 5 或 6 次。

      但在某些时候 在路上,另一个线程可能会来 并尝试访问数据。它 我会更好地准备 对于现在的这种可能性,而不是 试图追查神秘行为 稍后出现异常。

      因为我有一个 volatile 完成标志,保证在 之前设置 volatile 结果,并且唯一访问结果是通过 getter,如 smaple 中所述,一个例外如果调用 getter 并且操作尚未完成,则抛出,我希望 Completed 和结果 getter 可以由调用 DoSomething() 的线程以外的线程调用。无论如何,这就是我的希望。无论如何,我相信挥发物也是如此。

      【讨论】:

      • 我曾希望通过在样本中使用 volatiles 来确认或否认我的肯定推测,这将允许第三个线程也获取结果值,而不必担心返回未初始化的数据。
      【解决方案5】:

      根据您所展示的内容,我会说,不,该代码中不需要volatiles

      ManualResetEvent 本身没有隐式内存屏障。然而,主线程正在等待信号的事实意味着它不能修改任何变量。至少,它在等待时不能修改任何变量。所以我想你可以说等待同步对象是一个隐式内存屏障。

      但是请注意,如果其他线程存在并且可以访问这些变量,则可以修改它们。

      从您的问题来看,您似乎错过了 volatile 所做的事情。 volatile 所做的只是告诉编译器该变量可能被其他线程异步修改,因此它不应该优化访问该变量的代码。 volatile 不会以任何方式同步对变量的访问。

      【讨论】:

      • 不,它不同步,但它确实改变了内存可见性规则,这正是这个问题的真正意义所在。 volatile 不仅仅是阻止编译器在 .NET 中优化代码(与 C++ 不同)。
      • 根据 C# 语言规范、MSIL Volatile 操作码的文档和 MSDN C++ 文档,.NET 中的 volatile 和本机 C++ 中的 volatile 执行完全相同的操作:在读取时强制“获取”语义并在写入时“释放”语义。
      • 当前的 C++ 标准(不是即将到来的 C++0x)不保证任何地方的内存可见性,更不用说关于易失性了。如果 MSDN 提到这一点,它要么是在谈论 MS 的实现,要么是在谈论即将到来的标准。
      • 那么你是说 volatile 改变或不改变内存可见性?您的第二条评论似乎与您的第一条意见相矛盾。
      • 第一个评论是关于 .NET,其中 volatile 确实改变了可见性。第二个是关于 C++,目前的标准不做任何保证,必须假设它不会改变可见性。
      猜你喜欢
      • 1970-01-01
      • 2023-03-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多