前言:阅读提示 本文 注重基础概念
- 本文讲的是c++的boost/asio库 io-context是一个io管理器
- 了解多线程知识
异步
异步(Asynchronous)是一种编程和操作模式,与同步(Synchronous)相对。下面从概念、特点、应用场景等方面详细介绍异步
概念
在同步操作中,程序按顺序依次执行每个任务,一个任务执行时会阻塞后续任务,直到该任务完成,程序才会继续执行下一个任务。例如,当程序发起一个文件读取操作,在同步模式下,程序会暂停执行后续代码,一直等待文件读取完成后才会继续执行后续逻辑。
而异步操作则允许程序在发起一个操作后,不等待该操作完成就继续执行后续代码。当异步操作完成时,会通过某种机制(如回调函数、事件通知等)告知程序操作已完成,程序再对操作结果进行处理。
注意
异步操作完成后的通知和结果处理与线程相关,
- 在单线程环境下,回调函数在调用 io_context.run() 的线程中执行;
- 在多线程环境下,io_context 会选择一个空闲线程来执行回调函数
特点
非阻塞
这是异步操作的核心特点。在发起异步操作后,程序不会被阻塞,可以立即执行其他任务,从而提高程序的执行效率和响应能力。例如,在网络编程中,发起一个网络请求后,程序可以继续处理其他用户输入或执行其他业务逻辑,而不需要等待网络请求的响应。
回调机制
在异步编程里,回调机制十分关键。由于异步操作不会立刻返回结果,程序在发起异步操作后会接着执行后续代码。当异步操作完成时,就通过回调函数来处理操作结果。比如在网络编程中,发起一个异步网络请求后,程序不会等待请求的响应,而是继续执行其他任务。当请求响应到达时,会调用预先注册的回调函数来处理响应数据。
io-context
概念
io_context 本质上是一个事件循环和任务调度器。在异步编程中,程序会发起各种异步操作(如异步读取、异步写入、异步连接等)(隐式注册),这些操作不会立即完成,而是在后台进行。io_context 会跟踪这些异步操作的状态,当操作完成或者发生错误时,它会调用预先注册的回调函数来处理结果。
工作原理
- 1 异步操作的注册
acceptor_.async_accept(new_session->socket(),void function
直接被注册了 - 2 事件循环的启动
通过调用 io_context.run() 方法来启动 io_context 的事件循环。事件循环是 io_context 的核心机制,它会不断地检查是否有异步操作完成。具体来说,事件循环会执行以下步骤: -
- 检查操作系统提供的 I/O 多路复用机制(如 Linux 下的 epoll、Windows 下的 IOCP 等),查看是否有已就绪的 I/O 事件。
-
- 如果有已就绪的 I/O 事件,说明对应的异步操作已经完成,io_context 会从内部的任务队列中取出与该操作关联的回调函数,并将其放入执行队列。
- -依次执行执行队列中的回调函数。
- 3 回调函数的执行
当 io_context 检测到异步操作完成时,会调用相应的回调函数。回调函数中可以处理操作的结果,例如读取到的数据、错误信息等。在回调函数中,还可以发起新的异步操作,形成一个连续的异步处理流程。
内部原理
- 1 任务队列
io_context 内部维护着一个任务队列,用于存储所有注册的异步操作及其对应的回调函数。当异步操作完成时,对应的回调函数会被放入任务队列中等待执行。 - 2 I/O 多路复用机制
io_context 依赖操作系统提供的 I/O 多路复用机制来高效地管理多个 I/O 操作。不同的操作系统提供了不同的 I/O 多路复用机制,例如 Linux 下的 select、poll、epoll,Windows 下的 IOCP 等。io_context 会根据不同的操作系统选择合适的 I/O 多路复用机制,以提高性能。
线程交互
-
- 单线程情况
在单线程环境中,只有一个线程调用 io_context.run() 方法,所有的异步操作的回调函数都会在这个线程中执行。这种情况下,io_context 的事件循环会依次处理完成的异步操作,调用相应的回调函数。
- 单线程情况
-
- 多线程情况
在多线程环境中,多个线程可以同时调用 io_context.run() 方法。这些线程会共同参与 io_context 的事件循环,当有异步操作完成时,io_context 会选择一个空闲的线程来执行该操作的回调函数。这样可以充分利用多核 CPU 的并发能力,提高程序处理异步操作的效率。但需要注意的是,在多线程环境中,要处理好线程安全问题,避免多个线程同时访问和修改共享资源。
- 多线程情况
原子操作
原子操作的概念
原子操作指的是那些在执行过程中不可被中断的操作,即在多线程或多任务环境下,一个原子操作要么完全执行,要么完全不执行,不会出现执行到一半被其他线程干扰的情况。在 C++ 中,标准库提供了 头文件,其中定义了各种原子类型和原子操作,例如 std::atomic 可以实现对整数的原子操作。
原子操作在异步编程中的作用
保证数据一致性
在异步编程中,多个线程或任务可能会同时访问和修改共享数据。如果没有合适的同步机制,就可能会出现数据竞争问题,导致数据不一致。原子操作可以保证对共享数据的操作是原子的,从而避免数据竞争。
示例场景:假设有一个计数器 count,多个线程可能会同时对其进行递增操作。如果使用普通的整数类型,在多线程环境下可能会出现数据竞争。而使用原子类型 std::atomic 可以确保递增操作是原子的,保证计数器的正确性。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> count(0);
void increment() {
for (int i = 0; i < 10000; ++i) {
++count;
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}
for (auto& thread : threads) {
thread.join();
}
std::cout << "Final count: " << count << std::endl;
return 0;
}
在这个示例中,std::atomic count 确保了 ++count 操作是原子的,多个线程同时对其进行递增操作时不会出现数据竞争,最终输出的计数器值是正确的。
实现无锁数据结构
原子操作可以用于实现无锁数据结构,避免使用传统的锁机制(如互斥锁)带来的性能开销。无锁数据结构可以在多线程环境下高效地工作,减少线程阻塞和上下文切换的开销。
示例场景:实现一个无锁的栈数据结构,多个线程可以同时进行入栈和出栈操作。通过原子操作可以确保栈的操作是线程安全的,同时避免了锁的使用,提高了性能。
同步异步任务
在异步编程中,有时候需要确保某些操作按顺序执行,或者需要等待某个异步任务完成后再继续执行后续操作。原子操作可以用于实现简单的同步机制,例如使用原子标志位来表示某个任务是否完成。