6-异步运行时

236 阅读16分钟

如果这篇文章有帮到你,能给我一个 star 吗🥰 👉 github.com/night-cruis…

在前面的章节中,我们讲到过异步运行时负责调度执行使用者创建的 Future,那么异步运行时到底是如何工作的呢?在本章中,我们将会实现一个简单的单线程异步运行时,提供异步的网络IO读写操作,以探讨运行时的具体工作机制。

在正式开始之前,我们首先明确一下即将实现的运行时的工作原理:

  1. 用户使用 async fn 或者 async {} 的方式创建 Non-Leaf Future,然后使用 spawn 方法创建一个异步 task,并将这个 task 发送到 executor 的任务队列中。
  2. executor 从 task_queue 中取出 task,调用task 的 poll 方法,驱动 Non-Leaf Future 开始执行(如果已经开始执行了,则从上次的 await 断点处继续执行),就这样一直执行 Future 中的代码,直到遇到 Leaf Future.await
  3. 调用 Leaf Future 的 poll 方法,如果 Leaf Future 对应的IO事件已经就绪,则直接返回 Poll::Ready(data);如果对应的IO事件没有就绪,则调用 Reactor 的 register 方法注册等待的IO事件和 waker,然后 Poll::PendingNon-Leaf Future 将会被挂起),executor 可以继续执行其他的 task
  4. Reactor 会把注册的文件描述符 fdwaker 保存在BTreeMap<fd, waker> 中,然后调用 Epoll 提供的方法注册在 fd 上想要等待的 event 到 Epoll 系统中。
  5. Reactor 调用 Epoll 提供的 wait 方法获取所有就绪的文件描述符 fds,然后遍历 fds,通过 fd 匹配之前在 BTreeMap 中存储的 waker,然后调用 waker 的 wake 方法把 task 发送到 executor 的执行队列中,这样之前挂起的 Non-Leaf Future 就能够继续执行了。

通过上面的原理讲解我们可以知道,异步代码之所以高效的原因就是避免了IO对线程的阻塞:

  • 当执行一个 task 时,如果遇到了没有就绪的 IO 操作,就注册 waker 到 Reactor 中,然后挂起这个 taskexecutor 就可以继续执行其他的 task
  • 当 task 等待的 IO 事件就绪时,Reactor 就会通过 waker 唤醒关联的 task,然后就可以执行之前挂起的 task 了。

epoll

就像 Epoll servere example 一节中那样,为了方便地调用 libc 提供的 api,我们先创建一个 syscall 宏:

macro_rules! syscall {
    ($fn: ident ( $($arg: expr),* $(,)* ) ) => {{
        let res = unsafe { libc::$fn($($arg, )*) };
        if res == -1 {
            Err(io::Error::last_os_error())
        } else {
            Ok(res)
        }
    }};
}

Epoll

抽象出 Epoll 和 EpollEventType 类型:

pub(crate) struct Epoll {
    fd: RawFd,
}

pub(crate) enum EpollEventType {
    // Only event types used in this example
    In,
    Out,
}

RawFd 表示原始文件描述符。

方法实现

new

创建一个 Epoll 实例:

pub(crate) fn new() -> io::Result<Self> {
    let fd = syscall!(epoll_create1(libc::EPOLL_CLOEXEC))?;
    Ok(Epoll { fd })
}

添加事件/修改事件

fn run_ctl(&self, epoll_ctl: libc::c_int, fd: RawFd, op: EpollEventType) -> io::Result<()> {
    let mut event: libc::epoll_event = unsafe { mem::zeroed() };
    event.u64 = fd as u64;
    event.events = match op {
        EpollEventType::In => libc::EPOLLIN as u32,
        EpollEventType::Out => libc::EPOLLOUT as u32,
    };

    let event_p: *mut _ = &mut event as *mut _;
    syscall!(epoll_ctl(self.fd, epoll_ctl, fd, event_p))?;

    Ok(())
}

pub(crate) fn add_event(&self, fd: RawFd, op: EpollEventType) -> io::Result<()> {
    self.run_ctl(libc::EPOLL_CTL_ADD, fd, op)
}

#[allow(dead_code)]
pub(crate) fn mod_event(&self, fd: RawFd, op: EpollEventType) -> io::Result<()> {
    self.run_ctl(libc::EPOLL_CTL_MOD, fd, op)
}

add_event 和 mod_event 都是通过调用run_ctl 方法实现的。在 run_ctl 方法中根据 op 类型设置要注册/修改的事件类型,然后调用 epoll_ctl 方法来注册/修改事件。

删除事件

pub(crate) fn del_event(&self, fd: RawFd) -> io::Result<()> {
    syscall!(epoll_ctl(
        self.fd,
        libc::EPOLL_CTL_DEL,
        fd,
        std::ptr::null_mut() as *mut libc::epoll_event
    ))?;

    Ok(())
}

删除在 Epoll 实例中注册描述符 fd

等待就绪事件

pub(crate) fn wait(&self, events: &mut [libc::epoll_event]) -> io::Result<usize> {
    let nfd = syscall!(epoll_wait(
        self.fd,
        events.as_mut_ptr(),
        events.len() as i32,
        -1
    ))?;

    Ok(nfd as usize)
}

调用 epoll_wait 函数获取所有就绪的文件描述符,并将就绪的描述符存放到 events 中,最后返回就绪的描述符数量。

关闭Epoll

为 Epoll 实现 Drop trait,在清理 Epoll 时关闭 Epoll 的文件描述符:

impl Drop for Epoll {
    fn drop(&mut self) {
        syscall!(close(self.fd)).ok();
    }
}

reactor

Reactor

pub(crate) struct Reactor {
    pub epoll: Epoll,
    pub wakers: Mutex<BTreeMap<RawFd, Waker>>,
}

字段 epoll 存储创建的 Epoll 实例,wakers 存储等待的IO事件的文件描述符和对应的 waker

我们稍后将会创建 Epoll 的静态变量,为了内部可变性,就把 BTreeMap<RawFd, Waker> 包在 Mutex 中。

添加事件

impl Reactor {
    pub(crate) fn add_event(&self, fd: RawFd, op: EpollEventType, waker: Waker) -> io::Result<()> {
        info!("(Reactor) add event: {}", fd);
        self.epoll.add_event(fd, op)?;
        self.wakers.lock().unwrap().insert(fd, waker);
        Ok(())
    }
}

在 Reactor 的添加事件的方法中,首先调用 epoll 的 add_event 方法注册文件描述符和监听的事件,然后把描述符和对应的 waker 存储在 BTreeMap<RawFd, Waker> 中。

reactor 循环

fn reactor_main_loop() -> io::Result<()> {
    info!("Start reactor main loop");
    let max_event = 32;
    let event: libc::epoll_event = unsafe { mem::zeroed() };
    let mut events = vec![event; max_event];
    let reactor = &REACTOR;

    loop {
        let nfd = reactor.epoll.wait(&mut events)?;
        info!("(Reactor) wake up. nfd = {}", nfd);

        #[allow(clippy::needless_range_loop)]
        for i in 0..nfd {
            let fd = events[i].u64 as RawFd;
            if let Some(waker) = reactor.wakers.lock().unwrap().remove(&fd) {
                info!("(Reactor) delete event: {}", fd);
                reactor.epoll.del_event(fd)?;
                waker.wake();
            }
        }
    }
}

在 reactor_main_loop 函数中,我们使用一个 loop 循环,在循环中调用 epoll 的 wait 方法获取所有就绪的 IO 事件的文件描述符,如果没有事件就绪,wait 方法就会阻塞 reactor 线程,避免 CPU 空转。

然后遍历就绪的描述符,从 wakers 中获取描述符对应的 waker,之后调用 epoll 的 delete_event 方法删除描述符,表示这个事件已经处理完毕。

最后,调用 waker 的 wake 方法,把因为等待IO事件而挂起的 task 发送到 executor 的执行队列中。

REACTOR 静态变量

lazy_static! {
    pub(crate) static ref REACTOR: Reactor = {
        // Start reactor main loop
        std::thread::spawn(move || {
            reactor_main_loop()
        });

        Reactor {
            epoll: Epoll::new().expect("failed to create epoll"),
            wakers: Mutex::new(BTreeMap::new())
        }
    };
}

Executor 在主线程运行,负责调度执行 task,而 reactor_main_loop 内部使用一个无线循环不断地获取就绪的 fd,并唤醒挂起的 task。为了避免 reactor_main_loop 阻塞 Executor,我们就开一个线程去执行 reactor_main_loop

async_io

在 async_io 模块中,我们将会创建 Leaf Future,异步化网络IO的监听和读写操作。

Ipv4Addr

pub struct Ipv4Addr(libc::in_addr);

impl Ipv4Addr {
    pub fn new(a: u8, b: u8, c: u8, d: u8) -> Self {
        Ipv4Addr(libc::in_addr {
            s_addr: ((u32::from(a) << 24)
                | (u32::from(b) << 16)
                | (u32::from(c) << 8)
                | u32::from(d))
            .to_be(),
        })
    }
}

Ipv4Addr 就是 IPv4 地址,new 方法负责创建一个 Ipv4Addr 类型。

TcpListener

pub struct TcpListener(RawFd);

impl TcpListener {
    // NOTE: bind() may be block. So this should be an async function in reality.
    pub fn bind(addr: Ipv4Addr, port: u16) -> io::Result<TcpListener> {
        let backlog = 128;
        let sock = syscall!(socket(
            libc::PF_INET,
            libc::SOCK_STREAM | libc::SOCK_CLOEXEC,
            0
        ))?;
        let opt: i32 = 1;
        syscall!(setsockopt(
            sock,
            libc::SOL_SOCKET,
            libc::SO_REUSEADDR,
            &opt as *const _ as *const libc::c_void,
            std::mem::size_of_val(&opt) as u32
        ))?;

        let sin: libc::sockaddr_in = libc::sockaddr_in {
            sin_family: libc::AF_INET as libc::sa_family_t,
            sin_port: port.to_be(),
            sin_addr: addr.0,
            ..unsafe { mem::zeroed() }
        };
        let addr_p: *const libc::sockaddr = &sin as *const _ as *const _;
        let len = mem::size_of_val(&sin) as libc::socklen_t;

        syscall!(bind(sock, addr_p, len))?;
        syscall!(listen(sock, backlog))?;

        info!("(TcpListener) listen: {}", sock);
        let listener = TcpListener(sock);
        listener.nonblocking()?;
        Ok(listener)
    }

    pub(crate) fn accept(&self) -> io::Result<TcpStream> {
        let mut sin_client: libc::sockaddr_in = unsafe { mem::zeroed() };
        let addr_p: *mut libc::sockaddr = &mut sin_client as *mut _ as *mut _;
        let mut len: libc::socklen_t = unsafe { mem::zeroed() };
        let len_p: *mut _ = &mut len as *mut _;
        let sock_client = syscall!(accept(self.0, addr_p, len_p))?;
        info!("(TcpStream)  accept: {}", sock_client);
        Ok(TcpStream(sock_client))
    }

    pub fn incoming(&self) -> Incoming<'_> {
        Incoming(self)
    }

    fn nonblocking(&self) -> io::Result<()> {
        let flag = syscall!(fcntl(self.0, libc::F_GETFL, 0))?;
        syscall!(fcntl(self.0, libc::F_SETFL, flag | libc::O_NONBLOCK))?;
        Ok(())
    }
}

impl Drop for TcpListener {
    fn drop(&mut self) {
        info!("(TcpListener) close : {}", self.0);
        syscall!(close(self.0)).ok();
    }
}


pub struct Incoming<'a>(&'a TcpListener);

impl<'a> Incoming<'a> {
    pub fn next(&self) -> AcceptFuture<'a> {
        AcceptFuture(self.0)
    }
}

bind 方法负责绑定传入的的 IpV4 地址和端口号,创建一个 TcpListener 实例,需要注意的是要把 TcpListener 设置为非阻塞:listener.nonblocking(),这样在调用 accept 方法接收客户端连接时才不会阻塞。

accept 方法负责接收到来的客户端连接,然后创建 TcpStream,如果没有连接到来就返回一个 io error

nonblocking 方法调用 libc::fcntl 函数把 TcpListener 设置为非阻塞。

incoming 方法把 TcpListener 的引用包在 Incoming 中,然后返回一个 Incoming 的实例。

Incoming 表示 TcpListener 接收连接的流式处理,每当我们想要接收一个新的连接时,就调用 next 方法返回一个 AcceptFuture(后面会讲这个)。

TcpStream

pub struct TcpStream(RawFd);

impl TcpStream {
    fn nonblocking(&self) -> io::Result<()> {
        let flag = syscall!(fcntl(self.0, libc::F_GETFL, 0))?;
        syscall!(fcntl(self.0, libc::F_SETFL, flag | libc::O_NONBLOCK))?;
        Ok(())
    }

    pub fn read<'a>(&'a self, buf: &'a mut [u8]) -> ReadFuture<'a> {
        ReadFuture(self, buf)
    }

    pub fn write<'a>(&'a self, buf: &'a [u8]) -> WriteFuture<'a> {
        WriteFuture(self, buf)
    }

    pub fn raw_fd(&self) -> RawFd {
        self.0
    }
}

impl Drop for TcpStream {
    fn drop(&mut self) {
        info!("(TcpStream)  close : {}", self.0);
        syscall!(close(self.0)).ok();
    }
}

nonblocking 方法调用 libc::fcntl 函数把 TcpStream 设置为非阻塞。

read/write 方法分别返回 RreadFture/WriteFuture,和上面的 AcceptFuture 一样,我们将会在下面讲解这些 Future 的定义和作用。

Leaf Future

pub struct AcceptFuture<'a>(&'a TcpListener);
pub struct ReadFuture<'a>(&'a TcpStream, &'a mut [u8]);
pub struct WriteFuture<'a>(&'a TcpStream, &'a [u8]);

impl<'a> Future for AcceptFuture<'a> {
    type Output = Option<io::Result<TcpStream>>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        match self.0.accept() {
            Ok(stream) => {
                stream.nonblocking()?;
                Poll::Ready(Some(Ok(stream)))
            }
            Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
                REACTOR.add_event((self.0).0, EpollEventType::In, cx.waker().clone())?;
                Poll::Pending
            }
            Err(e) => Poll::Ready(Some(Err(e))),
        }
    }
}

impl<'a> Future for ReadFuture<'a> {
    type Output = io::Result<usize>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let res = syscall!(read(
            (self.0).0,
            self.1.as_mut_ptr() as *mut libc::c_void,
            self.1.len()
        ));
        match res {
            Ok(n) => Poll::Ready(Ok(n as usize)),
            Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
                REACTOR.add_event((self.0).0, EpollEventType::In, cx.waker().clone())?;
                Poll::Pending
            }
            Err(e) => Poll::Ready(Err(e)),
        }
    }
}

impl<'a> Future for WriteFuture<'a> {
    type Output = io::Result<usize>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let res = syscall!(write(
            (self.0).0,
            self.1.as_ptr() as *mut libc::c_void,
            self.1.len()
        ));
        match res {
            Ok(n) => Poll::Ready(Ok(n as usize)),
            Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
                REACTOR.add_event((self.0).0, EpollEventType::Out, cx.waker().clone())?;
                Poll::Pending
            }
            Err(e) => Poll::Ready(Err(e)),
        }
    }
}

在同步的处理方式中,监听 TcpListener 和读写 TcpStream 是阻塞式的,即会阻塞线程直到相应的 IO 事件发生;

而在异步的处理方式中,监听 TcpListener 和读写 TcpStream 不会阻塞掉线程,而是会返回一个对应的 Leaf Future

  • Incoming 的 next 方法会返回一个 AcceptFuture
  • TcpStream 的 read/write 方法分别返回 ReadFuture/WriteFuture

为 AcceptFuture/ReadFuture/WriteFuture 实现 Future trait,这样它们就有了 poll 方法。上述三个 Future 的 poll 方法的执行流程时类似的,因此下面我们只讲解 ReadFutrue 的执行流程。

当调用 ReadFutrue.await 时,会调用 ReadFutrue 的 poll 方法,在 poll 方法内部:

  • 调用 libc::read 函数从 TcpStream 中读取数据,返回 res

  • 匹配 res 的值:

    • 如果是 Ok(n),则读取到了数据,此时直接返回 Poll::Ready(OK(n)),调用方继续执行 ReadFutrue.await 下面的代码。
    • 如果是 Err(e),并且 e.kind() == io::ErrorKind::WouldBlock,则说明 TcpStream 中没有数据可读,这时就调用 REACTOR 的 add_event 方法注册文件描述符(关联读事件)和 waker。最后返回 Poll::Pending,调用方接收到 Poll::Pending 后就会调用 yield 表达式挂起当前的执行流(Task)。
    • 如果是其他的 Err(e),则说明读取数据发生了其他错误,此时返回 Poll::Ready(Err(e)) 表示读取失败,调用方继续执行 ReadFutrue.await 下面的代码。

当注册到 REACTOR 中的事件就绪时,REACTOR 就会使用注册的 waker 唤醒挂起的 Task,继续调用 ReadFutrue 的 poll 方法,重复上述的执行流程。

task

Task 是对 async fn 或者 async {} 创建的 Non-Leaf Future 的抽象,一个 task 就代表一个异步执行的任务:

#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub(crate) struct TaskId(u64);

impl TaskId {
    pub(crate) fn new() -> Self {
        static NEXT_ID: AtomicU64 = AtomicU64::new(0);
        TaskId(NEXT_ID.fetch_add(1, Ordering::Relaxed))
    }
}

pub(crate) struct Task {
    id: TaskId,
    future: Mutex<Pin<Box<dyn Future<Output = ()> + 'static + Send>>>,
    task_sender: Sender<Arc<Task>>,
}

Task 中有三个字段:

  • id:每个 task 都有一个唯一的 TaskIdTaskId 是有可能在不同的线程中创建的,因此使用原子类型 AtomicU64 来创建 TaskId 的实例,保证唯一性。
  • future:对用户创建的 Non-Leaf Future 的包装,使用 Pin 的目的是为了安全地使用自引用结构(Future 生成的状态机中可能存在自引用结构),使用 Mutex 的目的稍后讲解。
  • task_sender:一个 channel 的发送端,发送的 item 是 Arc<Task>,之所以使用 Arc<Task> 一方面是想要减小克隆 Task 的开销,另一方面与 Waker 的实现机制有关(稍后讲解)。

方法实现

impl Task {
    pub(crate) fn new(
        future: impl Future<Output = ()> + 'static + Send,
        task_sender: Sender<Arc<Task>>,
    ) -> Self {
        Task {
            id: TaskId::new(),
            future: Mutex::new(Box::pin(future)),
            task_sender,
        }
    }

    pub(crate) fn task_id(&self) -> TaskId {
        self.id
    }

    pub(crate) fn poll(&self, context: &mut Context) -> Poll<()> {
        self.future
            .lock()
            .expect("get lock failed")
            .as_mut()
            .poll(context)
    }
}

new 方法中传入参数 future 和 task_sender 后创建一个 Task 实例:

  • 参数 Future 要求满足 'static 生命周期是因为 task 的存在时间可能是任意长的,因此需要 Future 具有静态生命周期。
  • 要求 Future 满足 Send 是因为 Task 需要跨线程发送。
  • 由于 Future 最终使用 Mutex 包了起来,因此 future 字段最终同时满足 Send + Sync + 'static
  • Task 的其他两个字段也满足 Send + Sync + 'static ,因此 Task 满足 Send + Sync + 'static

由于 task_sender 发送的 item 是 Arc<Task>executor 的执行队列中收到的也是 Arc<Task>,因此 poll 方法的定义中只能使用 &self 不变引用。

又因为 Future 的 poll 方法调用需要可变引用,为了实现内部可变性,我们就用 Mutex 把 Pin<Box<Future>> 包了起来,这就是使用 Mutex 的原因。

在 poll 方法中,首先调用 self.future.lock() 获取锁,然后将调用 .as_mut() 方法获取 Pin<&mut dyn Future>,最后再调用 Future 中的 poll 方法执行 Future

实现 Wake trait

为 Task 实现 Wake trait,这样就可以通过 Task 来构建一个 Waker

impl Wake for Task {
    fn wake(self: Arc<Self>) {
        self.task_sender
            .send(self.clone())
            .expect("send task failed");
    }

    fn wake_by_ref(self: &Arc<Self>) {
        self.task_sender
            .send(self.clone())
            .expect("send task failed");
    }
}

Wake 中的 wake/wake_by_ref 方法实现就是具体的唤醒 task 的机制,在这个实现中,我们把想要唤醒的 task 通过 task_sender 发送到 executor 的执行队列中,这样 executor 就可以执行这个 task 了,这也是在 Task 定义中,需要 task_sender 字段的原因。

此外,wake/wake_by_ref 方法中都需要 Arc<Task>,这是 task_sender 的 item 类型为 Arc<Task> 的原因之一。

构造 Waker

对于实现了 Wake trait 的 Task,可以使用 std::task::Waker 的 from 方法构造一个 Waker

impl<W: Wake + Send + Sync + 'static> From<Arc<W>> for Waker {
    fn from(waker: Arc<W>) -> Waker {}
}

通过前面的分析,我们知道 Task 已经同时满足 Wake + Send + Sync + 'static,因此可以安全地使用 from 方法构造一个 Waker

executor

Executor

pub struct Executor {
    task_queue: Receiver<Arc<Task>>,
    waker_cache: BTreeMap<TaskId, Waker>,
}

task_queue 是一个 channel 的接收端,当 spawn 或者 wake 一个 task 时,就会发送 Arc<Task> 到 task_queue 中。

waker_cache 使用 BTreeMap 缓存可能会重复使用的 Waker,这是为了减小构造 Waker 的开销。

实际上,Executor 中的 task_queue 只是一个管道的接收端,并不是队列,只是我个人更习惯称之为队列。

方法实现

impl Executor {
    fn new(task_queue: Receiver<Arc<Task>>) -> Self {
        Self {
            task_queue,
            waker_cache: BTreeMap::new(),
        }
    }

    fn run_ready_task(&mut self) {
        while let Ok(task) = self.task_queue.recv() {
            let waker = self
                .waker_cache
                .entry(task.task_id())
                .or_insert_with(|| Waker::from(task.clone()));

            let mut context = Context::from_waker(waker);
            match task.poll(&mut context) {
                Poll::Ready(_) => {
                    self.waker_cache.remove(&task.task_id());
                }
                Poll::Pending => {}
            }
        }
    }

    pub fn run(&mut self) {
        self.run_ready_task();
    }
}

new 方法接收 Receiver<Arc<Task>> 参数,然后创建一个执行器实例。

run_ready_task 方法中:

  • 从 task_queue 中接收 task: Arc<Task>,然后从 waker_cache 中查找是否存在对应的 waker,如果没有则构造一个 Waker

  • 使用 Context 的 from_waker 方法通过 waker 的引用创建 context

  • 调用 task 的 poll 方法,传入 &mut context 参数,开始执行task

    • 如果返回的是 Poll::Ready,说明 task 执行完毕,从 waker_cache 中删除缓存的 waker
    • 如果返回的是 Poll::Pending,则什么都不做(最终执行的 Leaf-Future 中会注册等待的事件和 waker)。

Spawner

在初始状态下,executor 的执行队列中是空的,我们需要一种机制能够让用户手动地创建 task 并将 task 发送到 executor 的执行队列中,最后开启 executor 的执行。Spawner 抽象便提供了这种机制:

#[derive(Clone)]
pub struct Spawner {
    task_sender: Sender<Arc<Task>>,
}

Spawner 中的 task_sender 和 Task 的 task_sender 一样,都是为了把 task 发送到 executor 的执行队列中。

方法实现

impl Spawner {
    fn new(task_sender: Sender<Arc<Task>>) -> Self {
        Self { task_sender }
    }

    pub fn spawn(&self, future: impl Future<Output = ()> + 'static + Send) {
        let task = Task::new(future, self.task_sender.clone());
        self.task_sender
            .send(Arc::new(task))
            .expect("send task failed");
    }
}

new 方法接收Sender<Arc<Task>> 参数,然后创建一个 Spawner 实例。

spawn 方法中,使用传入的 future 参数创建一个 Task 实例,然后把这个 task 发送到 executor 的执行队列中。当 executor 开始执行的时候就可以从队列中接收 task,驱动 task 的执行了。

创建 Spawner & Executor

定义一个公开的函数,创建 Spawner 和 Executor 实例:

pub fn spawner_and_executor() -> (Spawner, Executor) {
    let (task_sender, task_queue) = bounded(10000);
    let spawner = Spawner::new(task_sender);
    let executor = Executor::new(task_queue);
    (spawner, executor)
}

在 spawner_and_executor 函数中,我们使用 crossbeam-channel 提供的 unbounded 函数创建一个容量为 10000 的管道,分别返回管道的发送端和接收端,然后创建 Spawner 和 Executor 实例并返回。

源代码仓库地址:github.com/night-cruis…

example

在这一节中,我们将使用之前实现的异步运行时创建一个 tcp echo server。需要导入的模块如下所示:

use std::env;
use std::io::Write;

use log::info;

use async_runtime::async_io::{Ipv4Addr, TcpListener, TcpStream};
use async_runtime::executor::{spawner_and_executor, Spawner};

日志打印

fn init_log() {
    // format = [file:line] msg
    env::set_var("RUST_LOG", "info");
    env_logger::Builder::from_default_env()
        .format(|buf, record| {
            writeln!(
                buf,
                "[{}:{:>3}] {}",
                record.file().unwrap_or("unknown"),
                record.line().unwrap_or(0),
                record.args(),
            )
        })
        .init();
}

init_log 函数是为了设置日志打印的消息格式,这跟异步运行时的使用没啥关系,这里就不再赘述了。

handle_client

async fn handle_client(stream: TcpStream) {
    let mut buf = [0u8; 1024];
    info!("(handle client) {}", stream.raw_fd());
    loop {
        let n = stream.read(&mut buf).await.unwrap();
        if n == 0 {
            break;
        }
        stream.write(&buf[..n]).await.unwrap();
    }
}

在 handle_client 函数中,我们首先创建一个 buf 数组,然后打印一个 handle client 的日志消息,接着开启一个无限循环:

  • 调用 stream.read(&mut buf) 方法后会返回一个 ReadFuture,在 ReadFuture 上调用 await 方法:

    • ReadFuture.await 会展开成一个无限循环,在循环内部会调用 ReadFuture 的 poll 方法。
    • 如果返回 Poll::Pending,则使用 Yield 表达式挂起当前的 task
    • 如果返回 poll::Ready 则中断循环并返回结果。
  • 如果 n== 0,则说明客户端已经断开了连接,则退出循环。

  • 调用 stream.write(&mut buf[.n]) 会返回一个 WriteFuture,在 WriteFuture 上调用 await 方法后执行流程与 ReadFuture 一致。

server_loop

async fn server_loop(spawner: Spawner) {
    let addr = Ipv4Addr::new(127, 0, 0, 1);
    let port = 8080;
    let listener = TcpListener::bind(addr, port).unwrap();

    let incoming = listener.incoming();

    while let Some(stream) = incoming.next().await {
        let stream = stream.unwrap();
        spawner.spawn(handle_client(stream));
    }
}

在 server_loop 函数中,我们调用 TcpListener 的 bind 方法创建一个 TcpListener 实例,然后调用 incoming 方法创建一个 Incoming 实例。

接着,在 While let 循环中,调用 incoming 的 next 方法返回一个 AcceptFuture 实例,在 AcceptFuture 上调用 await 方法后,如果返回的 Poll::Pending,则挂起当前的 task

当有客户端连接到来时,AcceptFuture 等待的 IO 事件就绪,会返回 io::Readult<TcpStream> 的实例并绑定到 stream 变量上,接着使用 spawner 调用 spawn 方法创建一个 task 处理与客户端的交互。

最后,又进入循环的开始位置,继续等待新的连接到来。

main 函数

fn main() {
    init_log();

    let (spawner, mut executor) = spawner_and_executor();

    spawner.spawn(server_loop(spawner.clone()));

    executor.run();
}

在 main 函数中,我们首先调用 init_log 函数设置日志消息格式,接着使用 spawner_and_executor 函数创建 Spawner 和 Executor 的实例。

然后调用 spawner.spawn 方法创建一个 task 用于执行 server_loop

最后调用 executor.run() 方法开启 executor 的运行,开始调度执行各个 task

运行示例

开启运行后的 echo server 会监听地址:127.0.0.1:8080

cargo run --example echo_server
    Finished dev [unoptimized + debuginfo] target(s) in 0.69s
     Running `target/debug/examples/echo_server`
[src/async_io.rs: 56] (TcpListener) listen: 3
[src/reactor.rs: 27] (Reactor) add event: 3
[src/reactor.rs: 35] Start reactor main loop

使用 Python 写一个小脚本模拟 TCP 客户端:

import socket
import threading

HOST = '127.0.0.1'
PORT = 8080


def send_request():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    for i in range(1, 1025):
        s.send(f"HELLO WORLD[{i}]".encode())
        data = s.recv(1024).decode()
        print(f"RECEIVE DATA: '{data}' in THREAD[{threading.currentThread().name}]")
    s.close()


def main():
    t_lst = []
    for _ in range(10):
        t = threading.Thread(target=send_request)
        t_lst.append(t)
        t.start()

    for t in t_lst:
        t.join()


if __name__ == '__main__':
    main()

运行脚本,服务端会输出以下内容:

.....
.....
.....
[src/reactor.rs: 43] (Reactor) wake up. nfd = 2
[src/reactor.rs: 49] (Reactor) delete event: 6
[src/reactor.rs: 49] (Reactor) delete event: 7
[src/reactor.rs: 43] (Reactor) wake up. nfd = 3
[src/reactor.rs: 49] (Reactor) delete event: 9
[src/reactor.rs: 27] (Reactor) add event: 6
[src/reactor.rs: 49] (Reactor) delete event: 10
[src/reactor.rs: 49] (Reactor) delete event: 11
[src/reactor.rs: 43] (Reactor) wake up. nfd = 2

客户端的输出内容如下所示:

.....
.....
.....
RECEIVE DATA: 'HELLO WORLD[1022]' in THREAD[Thread-3]
RECEIVE DATA: 'HELLO WORLD[1015]' in THREAD[Thread-7]
RECEIVE DATA: 'HELLO WORLD[1013]' in THREAD[Thread-6]
RECEIVE DATA: 'HELLO WORLD[1021]' in THREAD[Thread-1]
RECEIVE DATA: 'HELLO WORLD[1023]' in THREAD[Thread-3]
RECEIVE DATA: 'HELLO WORLD[1008]' in THREAD[Thread-10]
RECEIVE DATA: 'HELLO WORLD[1014]' in THREAD[Thread-6]
RECEIVE DATA: 'HELLO WORLD[1016]' in THREAD[Thread-7]

可以看出,我们的 echo server 正确地返回了响应,wake up. nfd = 3 表示有3个事件同时就绪,这说明 server 确实在并发地处理多个请求!

本书上一个章节:Epoll