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

Posted

技术标签:

【中文标题】Boost.Asio 的同步和并发数据结构模式【英文标题】:Synchronized and concurrent data structure patterns with Boost.Asio 【发布时间】:2018-02-16 19:16:07 【问题描述】:

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

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

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

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

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

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

为了说明 3. 中的问题,这里是来自chat client example 的 sn-p:

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&amp; t)就打电话

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

其他操作也类似?还是boost::asio::dispatch 更合适?

    最后,我知道并发向量、哈希映射等有很多健壮的实现(例如,Intel Thread Building Blocks Containers)。但似乎在受限制的用例下(例如,只维护一个活跃聊天参与者的列表、存储最近的消息等),完全并发的向量或哈希映射的力量可能有点矫枉过正。确实是这样,还是我最好只使用完全并发的数据结构?

【问题讨论】:

【参考方案1】:

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

STL(或类似 STL)容器(例如,std::deque、std::unordered_map);和

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

与细粒度锁定(在将 STL 数据结构作为 ActiveObjects 进行时自动选择)不同,使用粗粒度锁定总能获得更好的性能。

即使在设计中越早,通过减少共享比通过“优化同步”(这是矛盾的)来获得更高的性能。

免等待容器,例如 boost::lockfree::spsc_queue 或 folly::ProducerConsumerQueue。

您为什么还要同步对免等待容器的访问。无需等待意味着没有同步。

子弹

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

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

    请参阅上面的咆哮。

    为了使无需等待的读写容器适应同步、并发使用,将其操作包装在两个不同的链中是否足够,一个用于读取操作,一个用于写入操作?

    如前所述,同步对无锁结构的访问是愚蠢的。

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

    特别与 SPSC 队列相关,即在执行读/写操作的线程上放置了额外的约束。

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

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

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

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

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

    是的,这就是 ActiveObject 模式。但是,请考虑如何实现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() 之前提交,这可能不是您所期望的。此外,非常明显的是,所有细粒度同步步骤的效率都远低于对一组数据结构进行所有操作的链。

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

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


您希望 strands 形成执行的逻辑线程。您希望同步访问到您的数据结构,而不是您的数据结构本身。有时,无锁数据结构是避免设计良好的简单选择,但不要指望它神奇地表现良好。

一些链接:

boost::asio and Active Object(Tanner Sansbury on ActiveObject with Asio)有很多与您的问题重叠的想法 How to design proper release of a boost::asio socket or wrapper thereof 是我维护连接列表的示例

【讨论】:

感谢您的回答和链接中的参考——您的示例连接列表中的代码很有指导意义,对活动对象模式的参考很有用。我现在看到,我的“把一条线扔进一切”的方法是非常错误的。 (对不起,明显断开连接,我不小心按回车提交了此评论,然后快速删除它,下面出现后续评论/小问题) 如果以下广泛的准则准确反映了您的答案,您是否介意发表评论:1. 对于像 asio 聊天服务器示例这样的类,如果它要运行多线程,则需要一个链来协调套接字操作和此链可用于同步对 msg 队列的访问(例如,async_read 一条消息;ReadHandler 将其写入队列);和 2. 对于像您的 basic_connection_pool 示例这样更通用的类,多线程需要互斥锁,因为我们无法对共存的连接池做出假设(q.v.,您的 MyDeque 反例) " 需要一个链来协调套接字操作" - 是的,除非有隐式链(参见另一个 Tanner 经典:Why do I need a strand per connection)。许多流“对话”遵循纯粹的顺序操作链,所以这将是一个隐式链...... 只有当您开始在连接上进行全双工或异步消息传递时,您才需要明确该链。在这里查看我的答案:which starts with synchronous code, moves on to implicit strand and finally adds the explicit strand. 感谢您的附加链接!我想我理解显式链相对于单个线程调用 io_context::run 或像 HTTP 这样的半双工协议,但是那个计时器示例进程/讨论非常有帮助。我确实针对全双工消息传递;我在最初的问题中没有提到这一点,但基本上我正在做一些背景研究来充实基于 ASIO 和 Beast 构建的 WebSocket 服务器/消息传递端点

以上是关于Boost.Asio 的同步和并发数据结构模式的主要内容,如果未能解决你的问题,请参考以下文章

Asio 异步和并发

boost::asio::ip::tcp实现网络通信的小例子

Boost Asio初探

boost asio

使用 boost::asio 丢弃数据

boost::asio: “strand”类型的同步原语有啥名字吗?