使用双缓冲区技术进行并发读写?
Using a double buffer technique for concurrent reading and writing?
我有一个相对简单的案例:
- 我的程序将通过 Websockets 接收更新,并将使用这些更新来更新它的本地状态。这些更新将非常小(通常 < 1-1000 字节 JSON 因此反序列化时间 < 1ms)但会非常频繁(高达 ~1000/s)。
- 同时,程序将从这个本地状态reading/evaluating输出结果。
- 这两项任务应该 运行 并行,并且在程序持续期间 运行,即永不停止。
- 本地状态大小相对较小,因此内存使用不是一个大问题。
棘手的部分是更新需要“原子地”发生,因此它不会从本地状态读取,例如,只写入了一半的更新。状态不限于使用原语,可以包含任意 类 AFAICT atm,所以我无法通过使用 Interlocked
原子操作这样简单的方法来解决它。我计划 运行 将每个任务都放在自己的线程上,因此在这种情况下总共有两个线程。
为了实现这个目标,我想使用双缓冲技术,其中:
- 它保留了两份状态副本,因此可以在写入另一份的同时读取一份。
- 线程可以通过使用锁来传达它们正在使用的副本。即写入器线程在写入时锁定副本; reader 线程在完成当前副本后请求访问锁; writer 线程看到 reader 线程正在使用它,因此它切换到其他副本。
- 写入线程跟踪它在当前副本上完成的状态更新,因此当它切换到另一个副本时它可以“赶上”。
这是这个想法的大致要点,但实际实施当然会有点不同。
我试图查找这是否是一个常见的解决方案,但找不到太多信息,所以这让我想知道这样的事情:
- 它可行吗,还是我遗漏了什么?
- 有没有更好的方法?
- 这是一个通用的解决方案吗?如果是这样,它通常被称为什么?
- (奖金)是否有我可以阅读与此相关主题的好资源?
几乎我觉得我已经 运行 进入了一个死胡同,我无法找到(因为我不知道要搜索什么)更多的资源和信息来查看这种方法是否是“好的”。我计划在 .NET C# 中编写此代码,但我认为这些技术和解决方案可以转换为任何语言。感谢所有见解。
如果我没理解错的话,写入本身是同步的。如果是这样,那么也许没有必要保留两个副本,甚至不需要使用锁。
也许这样的方法可行?
State state = populateInitialState();
...
// Reader thread
public State doRead() {
return makeCopyOfState(state);
}
...
// Writer thread
public void updateState() {
State newState = makeCopyOfState(state);
// make changes in newState
state = newState;
}
您实际上需要四个 buffers/objects。两本 buffers/objects 属于 reader,一份属于作者,一份在邮箱中。
reader -- 每次他在他的新对象上完成一组原子操作时,他使用互锁交换将他的旧对象句柄(指针或索引无关紧要)与邮箱交换.然后他查看新获得的对象,并将序列号与他刚刚读取(并且仍然持有)的对象进行比较,以找出哪个更新。
写入者 -- 将最新数据的完整副本写入他的对象,然后使用互锁交换将他新写入的对象与邮箱之一交换。
如您所见,作者可以随时窃取邮箱对象,但永远不会窃取 reader 正在使用的邮箱对象,因此读取操作保持原子性。 reader 可以随时窃取邮箱对象,但永远不会窃取作者正在使用的邮箱对象,因此写操作保持原子性。
只要互锁交换函数产生正确的内存栅栏(在编写器线程中完成的交换释放,reader 线程获取),对象本身可以是任意复杂的。
您似乎在使用 input-process-output pattern in a multithreaded pipeline。有时问题简单时,输入和处理阶段(或处理和输出阶段)合并。
您已经添加了一个 C# 标记,因此使用 BlockingCollection 之类的东西可能是输入和输出线程之间进行通信的有用方法。由于本地状态相对较小(您的话),因此将包含本地状态副本的数据对象从输入线程发布到输出线程可能是一个简单的解决方案。这遵循了满足原子要求的无共享哲学,因为当前状态的快照已排队。 “追赶”能力得到满足,因为队列包含状态更改的积压。
通常,Messaging Patterns and Conversation Patterns 是有用的资源,可帮助您了解在 2 个或多个线程(或进程、服务、服务器等)之间通信什么以及如何通信。
我有一个相对简单的案例:
- 我的程序将通过 Websockets 接收更新,并将使用这些更新来更新它的本地状态。这些更新将非常小(通常 < 1-1000 字节 JSON 因此反序列化时间 < 1ms)但会非常频繁(高达 ~1000/s)。
- 同时,程序将从这个本地状态reading/evaluating输出结果。
- 这两项任务应该 运行 并行,并且在程序持续期间 运行,即永不停止。
- 本地状态大小相对较小,因此内存使用不是一个大问题。
棘手的部分是更新需要“原子地”发生,因此它不会从本地状态读取,例如,只写入了一半的更新。状态不限于使用原语,可以包含任意 类 AFAICT atm,所以我无法通过使用 Interlocked
原子操作这样简单的方法来解决它。我计划 运行 将每个任务都放在自己的线程上,因此在这种情况下总共有两个线程。
为了实现这个目标,我想使用双缓冲技术,其中:
- 它保留了两份状态副本,因此可以在写入另一份的同时读取一份。
- 线程可以通过使用锁来传达它们正在使用的副本。即写入器线程在写入时锁定副本; reader 线程在完成当前副本后请求访问锁; writer 线程看到 reader 线程正在使用它,因此它切换到其他副本。
- 写入线程跟踪它在当前副本上完成的状态更新,因此当它切换到另一个副本时它可以“赶上”。
这是这个想法的大致要点,但实际实施当然会有点不同。
我试图查找这是否是一个常见的解决方案,但找不到太多信息,所以这让我想知道这样的事情:
- 它可行吗,还是我遗漏了什么?
- 有没有更好的方法?
- 这是一个通用的解决方案吗?如果是这样,它通常被称为什么?
- (奖金)是否有我可以阅读与此相关主题的好资源?
几乎我觉得我已经 运行 进入了一个死胡同,我无法找到(因为我不知道要搜索什么)更多的资源和信息来查看这种方法是否是“好的”。我计划在 .NET C# 中编写此代码,但我认为这些技术和解决方案可以转换为任何语言。感谢所有见解。
如果我没理解错的话,写入本身是同步的。如果是这样,那么也许没有必要保留两个副本,甚至不需要使用锁。
也许这样的方法可行?
State state = populateInitialState();
...
// Reader thread
public State doRead() {
return makeCopyOfState(state);
}
...
// Writer thread
public void updateState() {
State newState = makeCopyOfState(state);
// make changes in newState
state = newState;
}
您实际上需要四个 buffers/objects。两本 buffers/objects 属于 reader,一份属于作者,一份在邮箱中。
reader -- 每次他在他的新对象上完成一组原子操作时,他使用互锁交换将他的旧对象句柄(指针或索引无关紧要)与邮箱交换.然后他查看新获得的对象,并将序列号与他刚刚读取(并且仍然持有)的对象进行比较,以找出哪个更新。
写入者 -- 将最新数据的完整副本写入他的对象,然后使用互锁交换将他新写入的对象与邮箱之一交换。
如您所见,作者可以随时窃取邮箱对象,但永远不会窃取 reader 正在使用的邮箱对象,因此读取操作保持原子性。 reader 可以随时窃取邮箱对象,但永远不会窃取作者正在使用的邮箱对象,因此写操作保持原子性。
只要互锁交换函数产生正确的内存栅栏(在编写器线程中完成的交换释放,reader 线程获取),对象本身可以是任意复杂的。
您似乎在使用 input-process-output pattern in a multithreaded pipeline。有时问题简单时,输入和处理阶段(或处理和输出阶段)合并。
您已经添加了一个 C# 标记,因此使用 BlockingCollection 之类的东西可能是输入和输出线程之间进行通信的有用方法。由于本地状态相对较小(您的话),因此将包含本地状态副本的数据对象从输入线程发布到输出线程可能是一个简单的解决方案。这遵循了满足原子要求的无共享哲学,因为当前状态的快照已排队。 “追赶”能力得到满足,因为队列包含状态更改的积压。
通常,Messaging Patterns and Conversation Patterns 是有用的资源,可帮助您了解在 2 个或多个线程(或进程、服务、服务器等)之间通信什么以及如何通信。