【问题标题】:Making variables captured by a closure volatile使闭包捕获的变量易变
【发布时间】:2012-02-23 12:29:23
【问题描述】:

闭包捕获的变量如何与不同的线程交互?在下面的示例代码中,我想将 totalEvents 声明为 volatile,但 C# 不允许这样做。

(是的,我知道这是不好的代码,这只是一个示例)

private void WaitFor10Events()
{
     volatile int totalEvents = 0; // error CS0106:

     _someEventGenerator.SomeEvent += (s, e) => totalEvents++;

     while(totalEvents < 10)
        Thread.Sleep(100);
}

编辑:人们似乎有点忽略了我的问题。我知道我不能在本地变量上使用volatile。我也知道示例代码代码很糟糕,可以通过其他方式实现,因此我的“错误代码”免责声明。这只是为了说明问题。

无论如何,似乎没有办法将易失语义强制到捕获的局部变量上,所以我将实现一种不同的方式。不过感谢您的回答,无论如何,我学到了一些有用的东西。 :)

【问题讨论】:

  • 你希望通过声明变量volatile 获得什么?在大多数情况下,volatile 的用户希望它做任何事情,而不是实际做的事情。
  • @Mr.Disappointment - OP 知道这一点 - 这就是为什么他/她说“我想将 totalEvents 声明为 volatile,但 C# 不允许这样做”
  • @Mr.Disappointment - 我相信 OP 确实知道原因 - 这是一个有趣的问题。我认为 OP 试图理解的是:在类级别,由于某些编译器优化,您可以将 volatile 应用于一个字段,以确保它可以被多个线程安全地读取和写入(都看到最新值)没有被应用。在上面的示例中,OP 正在使用闭包模拟类似的情况。在该示例中,totalEvents 可以被多个线程修改。问题是“你能在这种情况下得到同样的行为吗?如果能,怎么做?”
  • @GazTheDestroyer - 我知道你知道 - 我说的是“OP确实理解这一点并且提出了一个好问题”
  • @RobLevine:哎呀,抱歉 Rob,我的用户名弄混了。

标签: c# closures volatile


【解决方案1】:

Volatile.Write 救援:

private void WaitFor10Events()
{
     int totalEvents = 0; 

     _someEventGenerator.SomeEvent += (s, e) => Volatile.Write(ref totalEvents, totalEvents+1);

     while(totalEvents < 10)
        Thread.Sleep(100);
}

也就是说,对于这种特殊情况,我仍然会使用 Interlocked.Increment..

【讨论】:

    【解决方案2】:

    将局部变量标记为 volatile 是无效的。闭包可以捕获 volatile 字段,以下是完全合法的:

    volatile int totalEvents = 0;
    private void WaitFor10Events()
    {
       _someEventGenerator.SomeEvent += (s, e) => totalEvents++;
       ...
    }
    

    请参阅here for information 了解 volatile 关键字;

    顺便说一句,您可以考虑使用重置事件(automanual)、监视器类(pulsewait 方法)或 countdown event 让线程休眠直到发生事件被提升了,它比在循环中休眠要高效得多。

    更新

    根据问题的编辑,获得线程安全语义的一种简单方法是使用Interlocked class。以这种方式重写您的示例(尽管如其他答案中所述,有更好的方法来编写此示例):

    private void WaitFor10Events()
    {
       long totalEvents = 0;
       _someEventGenerator.SomeEvent += (s, e) => Interlocked.Increment(ref totalEvents);
    
       while(Interlocked.Read(ref totalEvents) < 10)
       {
         Thread.Sleep(100);
       }
    }
    

    【讨论】:

      【解决方案3】:

      你不能声明本地人volatile。此外,还有更好的方法来实现您的目标......请改用System.Threading.CountdownEvent。它会比你的 poll/sleep 方法更有效。

      using(CountdownEvent cde = new CountdownEvent(10))
      {
        _someEventGenerator.SomeEvent += (s, e) => cde.Signal();
        cde.Wait();
      }
      

      【讨论】:

      • 它没有回答问题。
      【解决方案4】:

      如果并行触发事件,这将不起作用。不幸的是,n++ 不是 .NET 中的原子操作,因此您不能期望多个线程执行 n++ 10 次实际上将 n 增加 10,它可以增加更少。这是一个证明它的小程序(在此过程中确保在并行使用时正确处理闭包):

      class Program
      {
          static volatile int _outer = 0;
          static void Main(string[] args)
          {
              int inner = 0;
      
              Action act_outer1 = () => _outer++; // Interlocked.Increment(ref _outer);
              Action act_inner1 = () => inner++;  // Interlocked.Increment(ref inner);
              Action<int> combined = (i) => { act_outer1(); act_inner1(); };
      
              Console.WriteLine("None outer={0}, inner={1}", _outer, inner);
              Parallel.For(0, 20000000, combined);
              Console.WriteLine("Once outer={0}, inner={1}", _outer, inner);
              Console.ReadKey();
          }
      }
      

      Interlocked.Increment 变体按预期工作。

      【讨论】:

        【解决方案5】:

        闭包捕获的局部变量将"hoisted" 输出到编译器生成的不同类,这样做不会放松“局部变量不能是易失性”规则,即使局部“真的”结束作为实例字段。最好的办法是手动构造闭包类(“函数对象”)或使用一些 IL 操作工具,例如。 ILDASM。

        【讨论】:

          猜你喜欢
          • 2016-01-28
          • 2014-03-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-10-14
          • 2012-04-23
          相关资源
          最近更新 更多