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

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

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

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

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

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

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

  1. 无保证读者看到最新值(没有内存屏障/易失性)
  2. 如果共享变量的类型不是基本类型或引用类型(例如,复合值类型),则读取器消耗的值可能无效。

直接的解决方案是锁定:

 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; } } }); 

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

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

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

 Func 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. primefaces性问题得到解决:由于闭包是一种参考类型,读/写根据标准保证了primefaces性
  3. 拳击成本:基本上使用闭包意味着以某种方式在堆上分配内存,这在每次迭代时都会发生。 不清楚这个的成本究竟是多少,但似乎比锁定更快……

还有什么我想念的吗? 您是否经常考虑将这种用途用于闭包而不是程序中的锁? 了解单读者/单作者共享状态的其他可能的快速解决方案也很棒。

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

Marc Gravell 非常好地certificate了无保护写入/读取的危险,其中读取线程从未观察到书面值。 最重要的是,如果您没有明确地同步访问权限,那么您的代码就会被破坏。

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

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

这些值可以在Tuple ,或者您可以编写自己的自定义类(您希望使其不可变,如Tuple ,以确保防止意外错误)。

当然,在您的第一个示例中,如果您确实使用了volatile语义,那么就会解决像int这样的类型的可见性问题,其中write和read可以primefaces方式完成。