C++核心技术:异步编程

4 分钟阅读 631 字 + 384 词
异步编程的基本概念
基本含义是 其操作的执行过程中当前执行线程仍能继续处理其他任务。
但是异步操作并不一定意味着新的(用户)线程。只要不妨碍当前线程的继续执行,都可以看作异步操作。
同步”的编程接口意味着在完成所需操作之前程序会阻塞。
“异步”的接口则行为有所不同:
  1. 允许不进行阻塞,而是让 用户代码查询操作的状态
  2. 通过回调的方式 通知用户代码某个操作已经完成。
但是,除了原始、性能一般的 select之外,不同操作系统提供的编程接口也大相径庭,有epoll、IOCP、kqueue、io_uring 等,让本来就困难的异步代码变得越发麻烦⋯…
异步编程麻烦,那为什么我们要进行异步编程呢?它的主要好处是高效的资源利用,比如, 可以在一个线程里管理成千上万个网络连接 。这样可以 提高吞吐量,降低延迟,并减少CPU 占用
ASIO
void doA(){
    doB();
    doC();
}
//如果doA不需要看到doB和doC的结果,也不需要按照这个顺序执行,可以使用asio这样写

asio::io_context context;
void doA(){
    context.post(doB);
    context.post(doC);
}
最简单的方式是在main里调用context.post(doB);和context.run(),那样run会阻塞地运行发布到context的所有任务,直到没有任务时退出。
我们可以想象简单的io_context实现
class io_context{
    public:
		using task_t = function<void()>;
		void post(task_t task)
        {
			task_queue_.push(std::move(task));
        }
		void run(){
            for (;;) {
				task_t task;
				if (task_queue_. empty)) {
					break;
                }
				task = std::move(task_queue_.front());
				task_queue_pop();
				task();
           }
	private:
		queue<task_t> task_queue_;
};
实际的io_context可以支持在多个线程里同时运行。
异步运行
异步的定时器回调代码:
void sayHello(){
	cout << "Hello, world! \n";
}
void doA(){
	auto t = make_shared<asio::steady_timer>(context, 2s);
	t-›async_wait([t] (const error_code& /*ec*/) { sayHello(); });//添加延迟执行的任务
}
//尤其要注意,下列代码是错误的
void doA(){
	auto t = asio::steady_timer(context, 2s);
	t.async_wait([t] (const error_code& /*ec*/) { sayHello(); });
}
如果使用这种形式的代码,运行时我们会看到 sayHello 直接运行了,没有等待两秒钟。 这是因为t在析构时会直接运行传入的函数对象 。这同时也是异步编程里一种常见的惯用法—— ==对于会超出作用域的对象,我们需要用 shared_ptr 在堆上创建,并用lambda表达式捕获下来,以确保在异步执行的回调结束执行之前,用到的对象不会消失。==
另外可以注意到,我们这里异步回调有一个参数 const error_code& ec,用来表示回调的原因。对于 steady_timer的回调,它可以表示成功,也可以表示操作被中断。当 steady_timer 对象在激发之前被提前析构,或者被主动 cancel 时,ec 就会被设成某个错误码,我们可以在回调里进行检查:
t->async_wait([t] (const error_code& /*ec*/) { 
    if (ec == asio::error::operation aborted) {
		return;
    sayHello(); 
});