C++优先考虑线程间的数据同步
C++ prioritize data synchronization between threads
我有一个场景,我在多个线程之间有一个共享数据模型。一些线程将循环写入该数据模型,而其他线程将循环读取该数据模型。但是可以保证写线程只写,reader个线程只读。
现在的情况是,由于 reader 端的实时限制,读取数据的优先级应高于写入数据。因此,例如,这是不可接受的。编写器锁定数据的时间过长。但是具有保证锁定时间的锁是可以接受的(例如,reader 在数据同步和可用之前最多等待 1 毫秒是可以接受的)。
所以我想知道这是如何实现的,因为“传统”锁定机制(例如 std::lock
)不会提供那些实时保证。
嗯,你有 readers,你有作家,你需要一把锁,所以...... a readers/writer lock 怎么样?
我提到 up-front 的原因是 (a) 你可能没有意识到它,但更重要的是 (b) C++ 中没有标准的 RW 锁(编辑:我的错误,添加了一个在 C++14 中),所以你对此的思考可能是在 std::mutex 的上下文中完成的。一旦决定使用 RW 锁,您就可以从其他人对这些锁的思考中获益。
特别是,有许多不同的选项可用于确定争用 RW 锁的线程的优先级。有一个选项,获取写锁的线程会一直等待,直到所有当前 reader 线程都释放锁,但是在编写器之后开始等待的 readers 在编写器完成它之前不会获得锁定。
使用该策略,只要编写器线程在每次事务后释放并重新获取锁,并且只要编写器在您的 1 毫秒目标内完成每个事务,readers 就不会饿死。
如果您的编写器不能承诺,那么除了重新设计编写器之外别无选择:要么在之前[=28=进行更多处理] 获取锁,或将事务拆分为多个部分,在每个部分之间删除锁是安全的。
另一方面,如果您的作者的交易花费 比 1 毫秒少得多,那么您可以考虑 跳过 release/reacquire 如果经过的时间少于 1 毫秒(纯粹是为了减少这样做的处理开销)……但我不建议这样做。在您的实施中添加复杂性和特殊情况以及(不寒而栗)挂钟时间 很少是最大化性能的最实用方法,并且会迅速增加错误的风险。简单的多线程系统才是可靠的多线程系统。
通常在这种情况下您使用 reader-writer-lock。这允许所有读取器并行读取或单个写入器写入。
但这并不能阻止作者在需要的情况下持有锁几分钟。强迫作者解锁也可能不是一个好主意。该对象可能在更改过程中处于某种不一致的状态。
还有另一种称为 read-copy-update 的同步方法可能会有所帮助。这允许作者修改元素而不会被读者阻止。缺点是您可能会让一些读者仍在阅读旧数据,而另一些读者在一段时间内阅读新数据。
如果多个作者试图更改同一个成员,那么他们也可能会有问题。较慢的编写器可能已经计算了所有需要的更新,只是为了注意到其他一些线程更改了对象。然后它必须重新开始浪费它已经花费的所有时间。
注意:复制元素可以在常数时间内完成,肯定在1ms以下。所以你可以保证读者不会被长时间阻塞。通过首先释放写锁,你可以保证读者在任意 2 次写操作之间读取,假设 RW 锁是按照相同的原则设计的。
所以我建议另一种解决方案,我称之为 write-intent-locking:
您从 RW 锁开始,但添加了一个锁来处理 write-intent。任何写者都可以随时获取write-intent锁,但只能获取其中一个锁,它是独占的。一旦写入持有 write-intent 锁,它就会复制元素
并开始修改副本。只要它想这样做就可以花很长时间,因为它不会阻止任何读者。不过它确实阻止了其他作者。
当所有修改都完成后,编写器获取写锁,然后用准备好的副本快速复制、移动或替换元素。然后它释放写入和 write-intent 锁,解锁想要访问同一元素的读者和作者。
我的处理方式是拥有两个相同的数据集副本;称他们为副本A和副本B。
读者总是从副本 B 读取,在访问它之前小心地以 read-only 模式锁定 reader/writer 锁。
当 writer-thread 想要更新数据集时,它会锁定副本 A(使用常规互斥锁)并更新它。 writer-thread 可以随心所欲地执行此操作,因为没有读者在使用副本 A。
当 writer-thread 完成更新副本 A 时,它会锁定 reader/writer 锁(在 exclusive/writer-lock 模式下)并将数据集 A 与数据集 B 交换。(此交换应该完成通过交换指针,因此 O(1) 快。
writer-thread 然后解锁 reader/writer 锁(这样任何等待的 reader-threads 现在可以访问更新的 data-set),然后更新另一个 data-set 与更新第一个 data-set 的方式相同。这也可能需要 writer-thread 喜欢的时间,因为没有 reader-threads 正在等待此数据集。
最后 writer-thread 解锁了常规互斥体,我们就完成了。
如果模型允许写入中断,那么它也允许缓冲。使用 fifo 队列并仅在已写入 50 个元素时才开始读取。使用(智能)指针交换 fifo 队列中的数据。交换 8 个字节的指针需要几纳秒。由于有缓冲,写入将在与 readers 正在处理的元素不同的元素上进行,因此只要生产者能够跟上生产者的步伐,就不会有锁争用。
为什么 reader 不生成自己的消费者数据?如果你可以有 n 个生产者和 n 个消费者,那么每个消费者也可以在没有任何生产者的情况下产生自己的数据。但是这会有不同的多线程缩放。也许你的算法在这里不适用,但如果适用,它更像是独立的 multi-processing 而不是 multi-threading.
作家的工作可以转换为多个较小的工作吗? writer 中的进度可以报告给原子计数器。当 reader 有等待预算时,它会检查原子值,如果它看起来很慢,它可以使用相同的原子值立即将其推到 100% 进度并且作者看到它并 early-quits 锁定。
我有一个场景,我在多个线程之间有一个共享数据模型。一些线程将循环写入该数据模型,而其他线程将循环读取该数据模型。但是可以保证写线程只写,reader个线程只读。
现在的情况是,由于 reader 端的实时限制,读取数据的优先级应高于写入数据。因此,例如,这是不可接受的。编写器锁定数据的时间过长。但是具有保证锁定时间的锁是可以接受的(例如,reader 在数据同步和可用之前最多等待 1 毫秒是可以接受的)。
所以我想知道这是如何实现的,因为“传统”锁定机制(例如 std::lock
)不会提供那些实时保证。
嗯,你有 readers,你有作家,你需要一把锁,所以...... a readers/writer lock 怎么样?
我提到 up-front 的原因是 (a) 你可能没有意识到它,但更重要的是 (b) C++ 中没有标准的 RW 锁(编辑:我的错误,添加了一个在 C++14 中),所以你对此的思考可能是在 std::mutex 的上下文中完成的。一旦决定使用 RW 锁,您就可以从其他人对这些锁的思考中获益。
特别是,有许多不同的选项可用于确定争用 RW 锁的线程的优先级。有一个选项,获取写锁的线程会一直等待,直到所有当前 reader 线程都释放锁,但是在编写器之后开始等待的 readers 在编写器完成它之前不会获得锁定。
使用该策略,只要编写器线程在每次事务后释放并重新获取锁,并且只要编写器在您的 1 毫秒目标内完成每个事务,readers 就不会饿死。
如果您的编写器不能承诺,那么除了重新设计编写器之外别无选择:要么在之前[=28=进行更多处理] 获取锁,或将事务拆分为多个部分,在每个部分之间删除锁是安全的。
另一方面,如果您的作者的交易花费 比 1 毫秒少得多,那么您可以考虑 跳过 release/reacquire 如果经过的时间少于 1 毫秒(纯粹是为了减少这样做的处理开销)……但我不建议这样做。在您的实施中添加复杂性和特殊情况以及(不寒而栗)挂钟时间 很少是最大化性能的最实用方法,并且会迅速增加错误的风险。简单的多线程系统才是可靠的多线程系统。
通常在这种情况下您使用 reader-writer-lock。这允许所有读取器并行读取或单个写入器写入。
但这并不能阻止作者在需要的情况下持有锁几分钟。强迫作者解锁也可能不是一个好主意。该对象可能在更改过程中处于某种不一致的状态。
还有另一种称为 read-copy-update 的同步方法可能会有所帮助。这允许作者修改元素而不会被读者阻止。缺点是您可能会让一些读者仍在阅读旧数据,而另一些读者在一段时间内阅读新数据。
如果多个作者试图更改同一个成员,那么他们也可能会有问题。较慢的编写器可能已经计算了所有需要的更新,只是为了注意到其他一些线程更改了对象。然后它必须重新开始浪费它已经花费的所有时间。
注意:复制元素可以在常数时间内完成,肯定在1ms以下。所以你可以保证读者不会被长时间阻塞。通过首先释放写锁,你可以保证读者在任意 2 次写操作之间读取,假设 RW 锁是按照相同的原则设计的。
所以我建议另一种解决方案,我称之为 write-intent-locking:
您从 RW 锁开始,但添加了一个锁来处理 write-intent。任何写者都可以随时获取write-intent锁,但只能获取其中一个锁,它是独占的。一旦写入持有 write-intent 锁,它就会复制元素 并开始修改副本。只要它想这样做就可以花很长时间,因为它不会阻止任何读者。不过它确实阻止了其他作者。
当所有修改都完成后,编写器获取写锁,然后用准备好的副本快速复制、移动或替换元素。然后它释放写入和 write-intent 锁,解锁想要访问同一元素的读者和作者。
我的处理方式是拥有两个相同的数据集副本;称他们为副本A和副本B。
读者总是从副本 B 读取,在访问它之前小心地以 read-only 模式锁定 reader/writer 锁。
当 writer-thread 想要更新数据集时,它会锁定副本 A(使用常规互斥锁)并更新它。 writer-thread 可以随心所欲地执行此操作,因为没有读者在使用副本 A。
当 writer-thread 完成更新副本 A 时,它会锁定 reader/writer 锁(在 exclusive/writer-lock 模式下)并将数据集 A 与数据集 B 交换。(此交换应该完成通过交换指针,因此 O(1) 快。
writer-thread 然后解锁 reader/writer 锁(这样任何等待的 reader-threads 现在可以访问更新的 data-set),然后更新另一个 data-set 与更新第一个 data-set 的方式相同。这也可能需要 writer-thread 喜欢的时间,因为没有 reader-threads 正在等待此数据集。
最后 writer-thread 解锁了常规互斥体,我们就完成了。
如果模型允许写入中断,那么它也允许缓冲。使用 fifo 队列并仅在已写入 50 个元素时才开始读取。使用(智能)指针交换 fifo 队列中的数据。交换 8 个字节的指针需要几纳秒。由于有缓冲,写入将在与 readers 正在处理的元素不同的元素上进行,因此只要生产者能够跟上生产者的步伐,就不会有锁争用。
为什么 reader 不生成自己的消费者数据?如果你可以有 n 个生产者和 n 个消费者,那么每个消费者也可以在没有任何生产者的情况下产生自己的数据。但是这会有不同的多线程缩放。也许你的算法在这里不适用,但如果适用,它更像是独立的 multi-processing 而不是 multi-threading.
作家的工作可以转换为多个较小的工作吗? writer 中的进度可以报告给原子计数器。当 reader 有等待预算时,它会检查原子值,如果它看起来很慢,它可以使用相同的原子值立即将其推到 100% 进度并且作者看到它并 early-quits 锁定。