Boost.Asio 多线程模型

Boost.Asio 有两种支持多线程的方式,第一种方式比较简单:在多线程的场景下,每个线程都持有一个 io_service,并且每个线程都调用各自的 io_service 的 run() 方法(io_service 的运行机制可以看本站的《深入理解 Boost.Asio 的 io_service》)。

另一种支持多线程的方式:全局只分配一个 io_service,并且让这个 io_service 在多个线程之间共享,每个线程都调用全局的 io_service 的 run() 方法。

每个线程一个 I/O Service

让我们先分析第一种方案:在多线程的场景下,每个线程都持有一个 io_service(通常的做法是,让线程数和 CPU 核心数保持一致)。这样,每个 io_service 都由独立的线程驱动,彼此之间互不竞争;代价是当某个 io_service 暂时没有 I/O 事件时,它对应的线程就只能空闲等待。下面我们实现了一个 AsioIOServicePool,封装了线程池的创建操作,完整代码如下:

#include <cstddef>
#include <memory>
#include <thread>
#include <vector>
#include <boost/asio.hpp>

class AsioIOServicePool {
public:
    using IOService = boost::asio::io_service;
    using Work = boost::asio::io_service::work;
    using WorkPtr = std::unique_ptr<Work>;

    AsioIOServicePool(std::size_t size = std::thread::hardware_concurrency())
        : ioServices_(size), works_(size), nextIOService_(0) {
        for (std::size_t i = 0; i < size; ++i) {
            works_[i] = std::unique_ptr<Work>(new Work(ioServices_[i]));
        }
        for (std::size_t i = 0; i < ioServices_.size(); ++i) {
            threads_.emplace_back([this, i] () { ioServices_[i].run(); });
        }
    }

    AsioIOServicePool(const AsioIOServicePool &) = delete;
    AsioIOServicePool &operator=(const AsioIOServicePool &) = delete;

    // 使用 round-robin 的方式返回一个 io_service
    boost::asio::io_service &getIOService() {
        auto &service = ioServices_[nextIOService_++];
        if (nextIOService_ == ioServices_.size()) {
            nextIOService_ = 0;
        }
        return service;
    }

    void stop() {
        for (auto &work: works_) {
            work.reset();
        }
        for (auto &t: threads_) {
            t.join();
        }
    }

private:
    std::vector<IOService>       ioServices_;
    std::vector<WorkPtr>         works_;
    std::vector<std::thread>     threads_;
    std::size_t                  nextIOService_;
};

AsioIOServicePool 使用起来也很简单:

#include <chrono>
#include <iostream>
#include <mutex>
#include <boost/asio.hpp>

std::mutex mtx; // 保护 std::cout

int main() {
    // 使用上文定义的 AsioIOServicePool
    AsioIOServicePool pool;

    boost::asio::steady_timer timer{pool.getIOService(), std::chrono::seconds{2}};
    timer.async_wait([&mtx] (const boost::system::error_code &ec) {
        std::lock_guard<std::mutex> lock(mtx);
        std::cout << "Hello, World! " << std::endl;
    });

    pool.stop();
    return 0;
}

一个 I/O Service 与多个线程

另一种方案则是先分配一个全局 io_service,然后开启多个线程,每个线程都调用这个 io_service 的 run() 方法。这样,当某个异步事件完成时,io_service 就会将相应的 event handler 交给任意一个线程去执行。

然而这种方案在实际使用中,需要注意一些问题:由于同一个 io_service 的事件处理器可能在多个线程中并发执行,因此会产生竞态条件。

下面实现了一个线程池,在每个 worker 线程中执行 io_service 的 run() 方法,完整代码如下:

#include <memory>
#include <thread>
#include <vector>
#include <boost/asio.hpp>

class AsioThreadPool {
public:
    AsioThreadPool(int threadNum = std::thread::hardware_concurrency())
        : work_(new boost::asio::io_service::work(service_)) {
        for (int i = 0; i < threadNum; ++i) {
            threads_.emplace_back([this] () { service_.run(); });
        }
    }

    AsioThreadPool(const AsioThreadPool &) = delete;
    AsioThreadPool &operator=(const AsioThreadPool &) = delete;

    boost::asio::io_service &getIOService() { return service_; }

    void stop() {
        work_.reset();
        for (auto &t: threads_) {
            t.join();
        }
    }

private:
    boost::asio::io_service service_;
    std::unique_ptr<boost::asio::io_service::work> work_;
    std::vector<std::thread> threads_;
};

无锁的同步方式

要怎样解决前面提到的竞态条件呢?Boost.Asio 提供了 io_service::strand:如果多个 event handler 通过同一个 strand 对象分发(dispatch),那么这些 event handler 就会保证串行地执行,不会并发地访问共享数据。

例如,下面的例子使用 strand,所以不需要使用互斥锁保证同步了,完整代码如下:

#include <chrono>
#include <iostream>
#include <boost/asio.hpp>

int main() {
    // 使用上文定义的 AsioThreadPool
    AsioThreadPool pool(4);    // 开启 4 个线程

    boost::asio::steady_timer timer1{pool.getIOService(), std::chrono::seconds{1}};
    boost::asio::steady_timer timer2{pool.getIOService(), std::chrono::seconds{1}};

    int value = 0;

    boost::asio::io_service::strand strand{pool.getIOService()};

    timer1.async_wait(strand.wrap([&value] (const boost::system::error_code &ec) {
        std::cout << "Hello, World! " << value++ << std::endl;
    }));

    timer2.async_wait(strand.wrap([&value] (const boost::system::error_code &ec) {
        std::cout << "Hello, World! " << value++ << std::endl;
    }));

    pool.stop();
    return 0;
}

多线程 Echo Server

下面的 EchoServer 可以在多线程中使用,它使用 io_service::strand 来解决前面提到的竞态问题,完整代码如下:

#include <array>
#include <iostream>
#include <memory>
#include <boost/asio.hpp>

using boost::asio::ip::tcp;

class TCPConnection : public std::enable_shared_from_this<TCPConnection> {
public:
    TCPConnection(boost::asio::io_service &io_service)
        : socket_(io_service), strand_(io_service) { }

    tcp::socket &socket() { return socket_; }

    void start() { doRead(); }

private:
    void doRead() {
        auto self = shared_from_this();
        socket_.async_read_some(
            boost::asio::buffer(buffer_, buffer_.size()),
            strand_.wrap([this, self](boost::system::error_code ec, std::size_t bytes_transferred) {
                if (!ec) { doWrite(bytes_transferred); }
            }));
    }

    void doWrite(std::size_t length) {
        auto self = shared_from_this();
        boost::asio::async_write(
            socket_,
            boost::asio::buffer(buffer_, length),
            strand_.wrap([this, self](boost::system::error_code ec, std::size_t /* bytes_transferred */) {
                if (!ec) { doRead(); }
            }));
    }

private:
    tcp::socket socket_;
    boost::asio::io_service::strand strand_;
    std::array<char, 8192> buffer_;
};

class EchoServer {
public:
    EchoServer(boost::asio::io_service &io_service, unsigned short port)
        : io_service_(io_service), acceptor_(io_service, tcp::endpoint(tcp::v4(), port)) {
        doAccept();
    }

    void doAccept() {
        auto conn = std::make_shared<TCPConnection>(io_service_);
        acceptor_.async_accept(conn->socket(), [this, conn](boost::system::error_code ec) {
            if (!ec) { conn->start(); }
            this->doAccept();
        });
    }

private:
    boost::asio::io_service &io_service_;
    tcp::acceptor acceptor_;
};

// 复用上文的 AsioThreadPool:多个线程共享同一个 io_service
int main() {
    AsioThreadPool pool;
    EchoServer server(pool.getIOService(), 8888);
    std::cin.get(); // 阻塞主线程,保持服务运行
    pool.stop();
    return 0;
}

关于单线程版本的回显服务端实现,可以对照本站的《Boost.Asio:客户端和服务端》。


   转载规则


《Boost.Asio 多线程模型》 由 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Boost property_tree:配置与数据解析 Boost property_tree:配置与数据解析
既然 Boost 提供了 property_tree 这样好的工具来给 C++ 解析 XML 和 JSON,必须要留有一定的篇幅来讲它。 原先项目中使用到了 JSON,需要用 C++ 解析一段返回值,JSON 格式如下: { "
2022-03-01
下一篇 
Boost.Asio:同步 vs 异步 Boost.Asio:同步 vs 异步
同步VS异步Boost.Asio的作者做了一个很惊艳的工作:它可以让你在同步和异步中自由选择,从而更好地适应你的应用。 在之前的章节中,我们已经学习了各种类型应用的框架,比如同步客户端,同步服务端,异步客户端,异步服务端。它们中的每一个都可
2022-02-15
  目录