与 Boost.Asio 同步和并发的数据结构模式

Synchronized and concurrent data structure patterns with Boost.Asio

本文关键字:数据 结构模式 并发 Boost Asio 同步      更新时间:2023-10-16

我正在寻找一些在将容器数据结构与 Boost.ASIO 一起使用时应用的指导原则。Boost.ASIO 文档介绍了如何使用strand对象来提供对共享资源的序列化访问,而无需显式线程同步。我正在寻找一种系统的方法将strand同步应用于:

STL(
  • 或类似STL)的容器(例如,std::dequestd::unordered_map);和
  • 免等待容器,例如boost::lockfree::spsc_queuefolly::ProducerConsumerQueue

我的问题列在下面。我应该提到,我的主要问题是1-3,但有理由,我也准备接受"这些问题没有实际意义/误导"作为答案;我在问题4中对此进行了详细说明。

  1. 要使任意 STL 容器适应安全同步使用,是否足以通过strand实例执行其所有操作?

  2. 为了使免等待读写容器适应同步、并发使用,是否足以通过两个不同的strand包装其操作,一个用于读取操作,一个用于写入操作?这个问题暗示了"是",尽管在该用例中,作者描述了使用strand来协调来自多个线程的生产者,而可能只从一个线程读取。

  3. 如果上面 1-2 的答案是肯定的,那么strand是否应该通过调用boost::asio::post来管理对数据结构的操作?

为了了解 3.中的问题,以下是聊天客户端示例中的代码片段:

void write(const chat_message& msg)
{
boost::asio::post(io_context_,
[this, msg]()
{
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(msg);
if (!write_in_progress)
{
do_write();
}
});
}

在这里,write_msgs_是聊天消息队列。我问是因为这是一种特殊情况,对post的调用可能会调用组合的异步操作(do_write)。如果我只是想从队列中推送或弹出怎么办?举一个高度简化的例子:

template<typename T>
class MyDeque {
public:
push_back(const T& t);
/* ... */
private:
std::deque<T> _deque;
boost::asio::io_context::strand _strand
};

那么MyDeque::push_back(const T& t)应该打电话

boost::asio::post(_strand, [&_deque]{ _deque.push_back(t); })

其他操作也是如此?还是boost::asio::dispatch更合适的选择?

  1. 最后,我知道有很多并发向量、哈希映射等的强大实现(例如,英特尔线程构建模块容器)。但似乎在受限的用例下(例如,仅维护活动聊天参与者的列表,存储最近的消息等),完全并发向量或哈希映射的力量可能有点矫枉过正。情况确实如此,还是我只使用完全并发的数据结构会更好?

我正在寻找一种系统的方法将链同步应用于:

STL(
  • 或类似STL)容器(例如,std::d eque,std::unordered_map);和

你正在寻找不存在的东西。最接近的是活动对象,除非对它们进行异步操作,否则您几乎不需要链。这几乎毫无意义,因为 STL 容器上的任何操作都不应具有足以保证异步的时间复杂度。另一方面,计算复杂性使得添加任何类型的同步都将是非常不理想的 -

而不是细粒度锁定[在将STL数据结构作为ActiveObjects时自动选择],您将始终发现粗粒度锁定的性能更好。

在设计中,通过减少共享比通过"优化同步"(这是一个矛盾)来获得更高的性能。

  • 免等待容器,如 boost::lockfree::spsc_queue 或 folly::P roducerConsumerQueue。

为什么您甚至要同步对免等待容器的访问。空闲等待意味着没有同步。

子弹

  1. 要使任意 STL 容器适应安全同步使用,是否足以通过链实例执行其所有操作?

    是的。只是那不是存在的东西。链包装异步任务(及其完成处理程序,它们只是来自执行者 POV 的任务)。

    请参阅上面的咆哮。

  2. 要使免等待读写容器适应同步、并发使用,是否足以通过两个不同的链(一个用于读取操作,一个用于写入操作)包装其操作?

    如前所述,将访问同步到无锁构造是愚蠢的。

    这个问题暗示了"是",尽管在该用例中,作者描述了使用链来协调来自多个线程的生产者,而可能只从一个线程读取。

    这与 SPSC 队列特别相关,即对执行读/写操作的线程施加了额外的约束。

    虽然这里的解决方案实际上是创建对任一一组操作具有独占访问权限的逻辑执行线程,但请注意,您正在约束任务,这与约束数据的角度根本不同。

  3. 如果上面 1-2 的答案是肯定的,那么链是否应该通过调用 boost::asio::p ost 来管理数据结构上的操作?

    所以,答案不是"是"。如我的介绍中所述,通过post发布所有操作的想法将归结为实现活动对象模式。是的,你可以这样做,不,这不会是聪明的。(我很确定,如果你这样做,根据定义,你可以忘记使用无锁容器)

    [....]
    那么MyDeque::p ush_back(const T&t)应该调用

    boost::asio::post(_strand, [&_deque]{ _deque.push_back(t); })
    

    是的,这就是活动对象模式。但是,请考虑如何实现top()。考虑一下,如果您有两个MyDeque实例(a和 'b)并希望将项目从一个移动到另一个实例,您会怎么做:

    if (!a.empty()) {
    auto value = a.top();     // synchronizes on the strand to have the return value
    a.pop();                  // operation on the strand of a
    b.push(std::move(value)); // operation on the strand of b
    }
    

    由于队列b不在a链上,b.push()实际上可以在a.pop()之前提交,这可能不是你所期望的。此外,很明显,所有细粒度的同步步骤的效率都远低于为处理一组数据结构的所有操作使用链。

  4. [...]但似乎[...]完全并发向量或哈希映射的力量可能有点矫枉过正。

    在完全并发的向量或哈希映射中没有"强制"。他们有成本(就处理器业务而言)和收益(就降低延迟而言)。在您提到的情况下,延迟很少是问题(注册会话是一个罕见的事件,并且由实际的 IO 速度主导),因此您最好使用最简单的东西(IMO 将是这些数据结构的单线程服务器)。让工作线程处理任何重要的操作 - 它们可以在线程池上运行。(例如,如果您决定实现下棋聊天机器人)


您希望链形成执行的逻辑线程。您希望同步对数据结构的访问,而不是对数据结构本身的访问。有时,无锁数据结构是一个简单的选择,可以避免必须很好地设计东西,但不要指望它神奇地表现良好。

一些链接:

  • boost::asio 和 Active Object(Tanner Sansbury on ActiveObject with Asio)有很多想法与您的问题重叠
  • 如何设计正确释放 boost::asio 套接字或其包装器是我维护连接列表的示例