Boost Asio Tutorial: Timer

发布时间 2023-08-24 16:00:30作者: 起风了oc

学一下C++的 Boost.Asio

Timer.1 同步的计时器

首先通过一个阻塞的计时器来了解一下 asio

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

int main() {
    // Asio 所有的程序都至少需要一个I/O执行上下文,比如 io_context 或者 thread_pool 对象
    // I/O执行上下文提供了对I/O功能的访问
    // 这里我们在主函数中首先声明一个io_context类型的对象。
    boost::asio::io_context io;
    // 接下来我们声明一个boost::asio::steady_timer对象
    // Asio中提供I/O功能的这些核心类,总是需要一个执行上下文的引用,作为其构造函数的第一个参数
    // steady_timer构造函数的第二个参数是一个duration类型的时间
    // 这里将计时器设置为从现在起 5 秒后到期
    boost::asio::steady_timer t(io, boost::asio::chrono::seconds(5));
    // 在这个简单的示例中,我们对计时器执行阻塞等待。也就是说,对 stable_timer::wait() 的调用不会立即返回,会一直等到计时器到期才会返回
    // 计时器始终处于两种状态之一:“过期”或“未过期”。如果在过期的定时器上调用 stable_timer::wait() 函数,它将立即返回。
    t.wait();
    // 最后,我们打印强制性的 "Hello, world!" 消息来显示计时器何时到期。
    std::cout << "Hello, world!" << std::endl;
    return 0;
}
Timer.2 异步的计时器

本教程程序演示了如何通过修改教程 Timer.1 中的程序来对计时器执行异步等待,从而使用 asio 的异步功能。

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

// 使用 asio 的异步功能,需要提供一个 Completion Token.
// Completion Token 确定当异步操作完成时如何将结果传递到回调函数。
// Completion Token 是一个抽象概念,一般情况下指的就是回调函数。
// 在此程序中,我们定义了一个名为 print 的函数,当异步等待完成时将调用该函数。
void print(const boost::system::error_code & /*e*/) {
    std::cout << "Hello, world!" << std::endl;
}

int main() {
    boost::asio::io_context io;
    boost::asio::steady_timer t(io, boost::asio::chrono::seconds(5));

    // 接下来,我们没有像教程 Timer.1 中那样进行阻塞等待,而是调用 stable_timer::async_wait() 函数来执行异步等待。
    // 调用此函数时,我们传递上面定义的 print 函数。
    t.async_wait(&print);
    // 最后,我们必须调用 io_context 对象的 io_context::run() 成员函数。
    // asio 库保证仅从当前正在调用 io_context::run() 的线程调用回调函数。
    // 因此,除非调用 io_context::run() 函数,否则回调函数永远不会被调用。
    // 当仍有“工作”要做时,io_context::run() 函数也将继续运行。
    // 在此示例中,工作是计时器上的异步等待,因此在计时器到期并且回调函数返回之前调用不会返回。
    // 重要的是要记住在调用 io_context::run() 之前给 io_context 一些工作。
    // 例如,如果我们注释了上面对 stable_timer::async_wait() 的调用,则 io_context 将没有任何工作要做,此时 io_context::run() 将立即返回。
    io.run();

    return 0;
}
Timer.3 将参数绑定到回调函数

在本教程中,我们将修改教程 Timer.2 中的程序,让计时器每秒触发一次。这将展示如何将额外参数传递给回调函数。

要实现这个计时器,需要启动一个持续1秒的计时器,当这个计时器到期之后,再重新启动这个计时器,将持续时间延长1秒。

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

// 修改回调函数,让回调函数能够访问计时器对象,并传递计时器延长次数
void print(const boost::system::error_code & /*e*/, boost::asio::steady_timer *t, int *count) {
    if (*count < 5) {
        std::cout << *count << std::endl;
        ++(*count);
        // 计时器启动时会记录一个时间点,假设这个时间点是10,告知计时器在1秒后停止,就是在11这个时间点停止
        // 当计时器进入下次事件循环,就会执行检查,对比当前时间是否大于等于11
        // 这样的实现,可以确保计时器按时到期,不会让其他语句执行产生的延迟影响到计时器,从而尽可能保证计时器的精度
        t->expires_at(t->expiry() + boost::asio::chrono::seconds(1));
        // 接下来再次启动计时器的异步等待
        // 如下所示,boost::bind() 函数用于将额外参数与回调函数关联起来
        // stable_timer::async_wait() 函数需要一个带有签名 void(const boost::system::error_code&) 的回调函数
        // 绑定附加参数会将您的 print 函数转换为与签名正确匹配的函数对象。
        t->async_wait(boost::bind(print,boost::asio::placeholders::error, t, count));
    }
}

int main() {
    boost::asio::io_context io;
    int count = 0;
    boost::asio::steady_timer t(io, boost::asio::chrono::seconds(1));
    t.async_wait(boost::bind(print, boost::asio::placeholders::error, &t, &count));

    io.run();
    std::cout << "Final count is " << count << std::endl;

    return 0;
}

如上所述,程序在计时器第六次触发时停止运行。但是你会发现程序并没有显式调用stop函数来要求 io_context 停止。回想一下,在教程 Timer.2 中,我们了解到 io_context::run() 函数在没有更多“工作”要做时便会返回。如果在 count 达到 5 时不在计时器上启动新的异步等待,io_context 将耗尽工作并自动停止运行。

在此示例中,boost::bind()boost::asio::placeholders::error 参数是传递给函数的error对象的命名占位符。当启动异步操作时,如果使用 boost::bind(),则必须仅指定与函数的参数列表匹配的参数。在教程 Timer.4 中,您将看到,如果回调函数不需要该参数,则可以忽略该占位符。

Timer.4 使用成员函数作为回调函数

在本教程中,我们将了解如何使用类成员函数作为回调函数。该程序的执行方式与教程 Timer.3 的程序相同。

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

// 我们现在定义一个名为 printer 的类,而不是像我们在之前的教程程序中那样定义一个自由函数 print 作为回调。
class printer {
public:
    // 构造函数引用 io_context 对象并在初始化 timer_ 成员时使用它。用于关闭程序的计数器现在也是该类的成员。
    explicit printer(boost::asio::io_context &io)
            : timer_(io, boost::asio::chrono::seconds(1)),
              count_(0) {

        // 类成员函数使用boost::bind()绑定参数,用法和自由函数一样。
        // 由于所有非静态类成员函数都有一个隐式 this 参数,因此我们需要将 this 绑定到该函数。
        // 与教程 Timer.3 中一样,boost::bind() 将我们的回调函数(现在是成员函数)转换为可以调用的函数对象,
        // 就好像它具有签名 void(const boost::system::error_code&) 一样。
        // 您会注意到,此处未指定 boost::asio::placeholders::error 占位符,
        // 因为 print 成员函数的参数列表没有声明error类型的参数(也就是说这个参数其实是可有可无的)。
        timer_.async_wait(boost::bind(&printer::print, this));
    }

    ~printer() {
        std::cout << "Final count is " << count_ << std::endl;
    }

    void print() {
        if (count_ < 5) {
            std::cout << count_ << std::endl;
            ++count_;

            timer_.expires_at(timer_.expiry() + boost::asio::chrono::seconds(1));
            timer_.async_wait(boost::bind(&printer::print, this));
        }
    }

private:
    boost::asio::steady_timer timer_;
    int count_;
};

int main() {
    boost::asio::io_context io;
    printer p(io);
    io.run();

    return 0;
}
Timer.5 同步多线程中的回调函数

本教程演示了如何使用strand类模板来同步多线程程序中的回调函数。

前四篇教程通过仅从一个线程调用 io_context::run() 函数来避免回调函数同步问题。如您所知,asio 库保证仅从当前正在调用 io_context::run() 的线程调用回调函数。因此,仅从一个线程调用 io_context::run() 可确保回调函数无法同时运行。

使用 asio 开发应用程序时,单线程方法通常是最好的起点。缺点是它在程序上的限制,尤其是服务器程序,包括:

  • 当回调函数需要很长时间才能完成时,响应速度很差。
  • 无法在多处理器系统上进行扩展,不能充分利用服务器的计算资源。

如果您发现自己遇到这些限制,另一种方法是使用一个调用 io_context::run() 的线程池。然而,由于这允许函数并发执行,因此当回调函数可能访问共享的、线程不安全的资源时,我们需要一种同步方法。

本课程将通过并行运行两个计时器来扩展上一教程。

#include <iostream>
#include <boost/asio.hpp>
#include <boost/thread/thread.hpp>
#include <boost/bind/bind.hpp>

class printer {
public:
    // 除了初始化一对 boost::asio::steady_timer 成员之外,构造函数还初始化了一个strand_成员,一个 boost::asio::strand 类型的对象。
    // strand 类模板是一个执行器适配器,对于通过它分派的回调函数,可以让正在执行的函数在下一个函数启动之前完成。
    // 无论调用 io_context::run() 的线程数量如何,都能保证这一点。
    // 当然,函数仍然可以与未通过链分派或通过不同链对象分派的其他函数同时执行。
    explicit printer(boost::asio::io_context &io)
            : strand_(boost::asio::make_strand(io)),
              timer1_(io, boost::asio::chrono::seconds(1)),
              timer2_(io, boost::asio::chrono::seconds(1)),
              count_(0) {
        // 当启动异步操作时,每个回调函数都“绑定”到 boost::asio::strand 对象。
        // boost::asio::bind_executor() 函数返回一个新的函数,该函数通过链对象自动分派其包含的函数。
        // 通过将函数绑定到同一链,我们确保它们不能同时执行。
        timer1_.async_wait(boost::asio::bind_executor(strand_, boost::bind(&printer::print1, this)));
        timer2_.async_wait(boost::asio::bind_executor(strand_, boost::bind(&printer::print2, this)));
    }

    ~printer() {
        std::cout << "Final count is " << count_ << std::endl;
    }

    // 在多线程程序中,如果异步操作的函数访问共享资源,则应同步它们。
    // 在本教程中,函数( print1 和 print2)使用的共享资源是 std::cout和 count_数据成员。
    void print1() {
        if (count_ < 10) {
            std::cout << "Timer 1: " << count_ << std::endl;
            ++count_;

            timer1_.expires_at(timer1_.expiry() + boost::asio::chrono::seconds(1));

            timer1_.async_wait(boost::asio::bind_executor(strand_,
                                                          boost::bind(&printer::print1, this)));
        }
    }

    void print2() {
        if (count_ < 10) {
            std::cout << "Timer 2: " << count_ << std::endl;
            ++count_;

            timer2_.expires_at(timer2_.expiry() + boost::asio::chrono::seconds(1));
            timer2_.async_wait(boost::asio::bind_executor(strand_, boost::bind(&printer::print2, this)));
        }
    }

private:
    boost::asio::strand<boost::asio::io_context::executor_type> strand_;
    boost::asio::steady_timer timer1_;
    boost::asio::steady_timer timer2_;
    int count_;
};

// 现在main函数中要从两个线程调用 io_context::run():主线程和一个附加线程(使用 boost::thread 对象创建)。
// 和单个线程调用一样,对 io_context::run() 的并发调用将在还有“工作”要做时继续执行。在所有异步操作完成之前,后台线程不会退出。

int main() {
    boost::asio::io_context io;
    printer p(io);
    boost::thread t(boost::bind(&boost::asio::io_context::run, &io));
    io.run();
    t.join();

    return 0;
}