使用闭包而不是锁用于共享状态的优缺点是什么?

时间:2015-03-21 10:13:26

标签: c# multithreading locking closures

我正在尝试评估在单作者,单读者情景中共享状态的最快解决方案,其中读者只消耗由指定的状态变量的最新值。的作家即可。共享状态可以是任何托管类型(即引用或值类型)。

理想情况下,同步解决方案的工作速度与尽可能简单的非同步解决方案一样快,因为该方法可能会用于单线程和多线程场景,可能需要数千次。

读/写的顺序无关紧要,只要阅读器在某个时间范围内收到最新值(即阅读器只会读取,永远不会修改,因此更新时间不会只要它在较旧的值之前没有收到未来值...}

天真的解决方案,没有锁定:

var memory = default(int);
var reader = Task.Run(() =>
{
    while (true)
    {
        func(memory);
    }
});

var writer = Task.Run(() =>
{
    while (true)
    {
        memory = DateTime.Now.Ticks;
    }
});

天真解决方案的真正问题是什么?到目前为止,我已经想到了这些:

  1. 无法保证读者看到最新值(无内存屏障/易失性)
  2. 如果共享变量的类型不是基本类型或引用类型(例如复合值类型),读取器消耗的值可能无效。
  3. 直接的解决方案是锁定:

    var gate = new object();
    var memory = default(int);
    var reader = Task.Run(() =>
    {
        while (true)
        {
            int local;
            lock(gate) { local = memory; }
            func(local);
        }
    });
    
    var writer = Task.Run(() =>
    {
        while (true)
        {
            lock(gate)
            {
                memory = DateTime.Now.Ticks;
            }
        }
    });
    

    这当然有效,但在单线程情况下会产生price of locking (~50ns),当然还会在多线程情况下产生上下文切换/争用的代价。

    对于大多数情况来说,这完全可以忽略不计,但在我的情况下这很重要,因为该方法将全面用于潜在的数千个循环,这些循环需要尽可能及时地每秒运行数万次。 / p>

    我想到的最后一个解决方案是使用不可变状态闭包来读取共享状态:

    Func<int> memory = () => default(int);
    var reader = Task.Run(() =>
    {
        while (true)
        {
            func(memory());
        }
    });
    
    var writer = Task.Run(() =>
    {
        while (true)
        {
            var state = DateTime.Now.Ticks;
            memory = () => state;
        }
    });
    

    现在这可能是什么问题?我自己的性能基准测试报告此解决方案与单线程情况下的锁定相比约为10ns。这似乎是一个很好的收获,但一些考虑因素包括:

    1. 仍然没有内存屏障/易失性,所以读者不能保证看到最新的关闭(这实际上有多常见?会很高兴知道...)
    2. 原子性问题得到解决:因为闭包是一种参考类型,读/写根据标准保证了原子性
    3. 拳击成本:基本上使用闭包意味着以某种方式在堆上分配内存,这在每次迭代时都会发生。不清楚这个的成本究竟是多少,但似乎比锁定更快......
    4. 还有什么我想念的吗?您是否经常考虑将这种用途用于闭包而不是程序中的锁?了解单读者/单作者共享状态的其他可能的快速解决方案也很棒。

1 个答案:

答案 0 :(得分:3)

正如您已经指出的那样,您的第一个和第三个示例都无法确保reader任务看到writer任务分配的最新值。一个缓解因素是,在x86硬件上,所有内存访问本质上都是易失性的,但是你的问题并没有将上下文限制为x86硬件,并且在任何情况下都假定写入或读取都没有优化由JIT编译器输出。

Marc Gravell有an excellent demonstration无保护写入/读取的危险,其中写入的值只是读取线程永远不会观察到。最重要的是,如果您没有明确地同步访问权限,那么您的代码就会被破坏。

所以,请使用第二个示例,这是唯一一个实际上正确的示例。


顺便说一句,就使用闭包来包装一个值而言,我会说没有意义。您可以直接在对象中有效地包装一些值集合,而不是让编译器为您生成类,并使用该对象的引用作为读取器和编写器的共享值。在对象引用上使用Thread.VolatileWrite()Thread.VolatileRead()解决了跨线程可见性问题(我假设你在这里使用捕获的本地...当然,如果共享变量是一个字段,你可以标记volatile)。

值可以在Tuple中,或者您可以编写自己的自定义类(您希望将其设置为Tuple,以确保不会发生意外错误)。

当然,在你的第一个例子中,如果你确实使用了volatile语义,那就解决了像int这样的类型的可见性问题,其中写和读可以原子方式完成。