我正在尝试评估在单写入器、单读取器场景中共享状态的最快解决方案是什么,其中读取器仅使用由作家。共享状态可以是任何托管类型(即引用或值类型)。
理想情况下,同步解决方案的工作速度与简单的非同步解决方案一样快,因为该方法可能会用于单线程和多线程场景数千次。
读/写的顺序并不重要,只要读者在某个时间范围内收到最新值(即读者只会读,不会修改,所以更新时间不会只要它没有在旧值之前接收到 future 值就很重要......)
简单的解决方案,没有锁定:
var memory = default(int);
var reader = Task.Run(() =>
{
while (true)
{
func(memory);
}
});
var writer = Task.Run(() =>
{
while (true)
{
memory = DateTime.Now.Ticks;
}
});
这个简单的解决方案到底存在哪些问题?到目前为止我已经想出了这些:
- 不保证读者看到最新值(无内存屏障/ volatile )
- 如果共享变量的类型不是基本类型或引用类型(例如复合值类型),则读取器使用的值可能无效。
最简单的解决方案是锁定:
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)在单线程情况下,当然还有多线程情况下上下文切换/争用的代价。
对于大多数情况来说,这完全可以忽略不计,但在我的情况下它很重要,因为该方法将全面用于潜在的数千个循环,这些循环需要尽可能及时地每秒运行数万次。
我想到的最后一个解决方案是使用不可变状态闭包来读取共享状态:
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;
}
});
现在这会带来什么潜在问题?我自己的性能基准报告显示,与单线程情况下的锁定相比,此解决方案大约需要 10 纳秒。这似乎是一个不错的收获,但一些考虑因素包括:
- 仍然没有内存屏障/ volatile ,因此读者不能保证看到最新的闭包(这实际上有多常见?很高兴知道......)
- 原子性问题得到解决:由于闭包是引用类型,读/写按照标准保证了原子性
- 装箱成本:基本上使用闭包意味着以某种方式在堆上分配内存,这在每次迭代中都会发生。不清楚这样做的真正成本是多少,但它似乎比锁定更快......
我还有什么遗漏的吗?您通常会考虑在程序中使用闭包而不是锁吗?了解单读取器/单写入器共享状态的其他可能的快速解决方案也将很高兴。
最佳答案
正如您已经指出的,您的第一个和第三个示例都无法确保读取器任务看到写入器任务分配的最新值。一个缓解因素是,在 x86 硬件上,所有内存访问本质上都是 volatile 的,但是您的问题没有任何内容将上下文限制为 x86 硬件,并且在任何情况下都假设写入或读取没有被 JIT 编译器优化。
Marc Gravell 有 an excellent demonstration未 protected 写/读的危险,其中读取线程永远观察不到写入的值。最重要的是,如果您不显式同步访问,您的代码就会损坏。
因此,请使用第二个示例,这是唯一真正正确的示例。
顺便说一句,就使用闭包来包装值而言,我认为这没有任何意义。您可以有效地将一些值集合直接包装在对象中,而不是让编译器为您生成类,并使用该对象的引用作为读取器和写入器的共享值。在对象引用上使用 Thread.VolatileWrite() 和 Thread.VolatileRead() 可以解决跨线程可见性问题(我假设您在此处使用捕获的本地…当然如果共享变量是一个字段,你可以将其标记为 volatile
)。
这些值可以位于Tuple
中,或者您可以编写自己的自定义类(您希望使其不可变,例如Tuple
,以确保防止意外错误)。
当然,在您的第一个示例中,如果您确实使用了 volatile 语义,则可以解决像 int
这样的类型的可见性问题,其中写入和读取可以原子地完成。
关于c# - 使用闭包而不是共享状态锁的优点和缺点是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29181517/