Tokio探秘(一)

本系列主要是了解Tokio的总结(方便自己以后回忆)。全文假设读者(以后的自己)已经了解Rust异步编程。

修改记录

目前为第一版,接下来会继续补充和优化。

概略

异步编程与同步最大的区别是通知机制。同步编程中,无论结果是否就绪,我们都可以得到结果的情况。一旦遍历到结果就绪,那么就去完成对应的逻辑。而异步编程不同。异步编程中,我们无需去遍历任务的结果。一旦结果就绪,任务会在后台执行完毕,并把结果通知给线程。

举个栗子,UNIX网络编程中,存在阻塞IO,非阻塞,多路复用,信号驱动这四种IO。这四种IO都可以划分为两个阶段。

  1. 等待数据就绪(等待结果是否就绪)
  2. 把网络数据从内核空间复制到用户空间(零拷贝这里不讨论)

四种IO尽管在第一阶段查看数据是否就绪的逻辑不同,但是一旦数据就绪,都需要自己去调用接口把数据从内核空间复制到用户空间。因此这四种IO都是同步IO。异步IO不同点在于系统会在数据就绪后自动把数据从内核空间复制到用户空间,再去通知用户线程。

使用异步编程之前,需要在异步编程入口开启一个Tokio运行时。

接下来的分析基于更复杂的多线程调度器。单线程调度器相差不大。

Rust提供了异步的关键字和trait,但是没有提供一套统一的异步调度机制。Tokio补充的就是这个。异步调度机制分为两个方面

  1. 任务
  2. 调度机制。这里包括线程池调度器和线程调度器。

首先先理清除调度机制。

顾名思义,线程池调度器基于线程池,线程调度器基于单线程。这里命名只是为了接下来更好的区分二者。

Tokio运行时会创建一个线程池,包含n个子线程(n可定义的),也称为工作线程。

Tokio为线程池实现一套线程管理机制,包括线程创建,运行,等待和关闭。在管理线程运行的逻辑中,线程池维持了一个全局任务队列。所有工作线程会从这个队列获取任务执行。在Tokio运行时创建代码中,Tokio提交了与线程数量1:1的调度器运行代码。每个工作线程都会执行一个调度器方法,表现为一个线程调度器。而线程池本身对外也表现为一个调度器。为了更好区分,我们称为线程池调度器。开发者一般只能看到线程池调度器,而线程调度器则仅对线程池调度器可见。提交到线程池调度器的异步任务最终都会按照某些规则交给其包含的线程调度器来进行调度。

线程池调度器本身会维护一个全局的异步任务队列。如果在非工作线程上提交异步任务,那么异步任务就会提交到这个全局的异步任务队列中。为了避免并发带来的数据竞争,全局异步任务队列会用Mutex进行加锁。

每个线程调度器本身还带有一个本地异步任务队列。如果是工作线程提交的异步任务,那么任务就会被提交到目前线程所运行的线程调度器的本地异步任务队列。线程调度器会循环去获取任务执行。一般会优先从本地队列中去获取异步任务。如果本地队列任务全部执行完毕,再去全局异步任务队列获取异步任务。为了避免全局异步任务队列中的异步任务长时间处于饥饿状态,线程会在执行一定个数的本地队列中的异步任务,优先去全局异步任务队列中获取异步任务。如果全局异步任务队列为空,则依旧从本地队列获取。

异步任务进入调度器后,第一次执行中,无论结果是否就绪,都会被移除出本地异步任务队列(有特殊情况)。如果第一次执行结果未就绪,一旦结果就绪,异步任务就会重新被放入本地异步任务队列等待二次执行。放入哪个工作线程本地队列取决于异步任务第一次执行所属的工作线程。

如果本地和全局异步任务队列都为空,线程调度器会尝试去其他工作线程“窃取”任务。处于“窃取”下的线程调度器会把自己的状态设置为searching。这个时候只要其他工作线程 的本地异步任务队列数量超过一半,线程就能开始“窃取”。本地异步任务队列中异步任务处理工作线程本身消费外,还会被其他工作线程“窃取”。因此本地异步任务队列也存在并发导致的数据竞争问题。Tokio使用原子操作来避免数据竞争。

上述操作下,线程调度器还是未获取到异步任务,调度器开始进入中断状态。这种状态下的线程调度器会进入如下一个工作循环:

  1. 处理时间和IO中断

    为了更优雅处理定时器和读写,Tokio封装了时间驱动和IO驱动。

    时间驱动的底层是实现了一个时间轮,专门来处理定时器事件。每定义一个定时器事件都会在时间轮上注册一个Entry。每次处理时间中断,Tokio都会推动时间轮前进,并把取出当前需要处理的定时器事件进行处理。

    IO驱动底层则是一个信号驱动IO,基于epollEventId实现。

    TODO 完善IO中断的简述,并在后面追加时间轮和IO的详细的描述。

  2. 维护线程调度器

    1. 释放完成的异步任务。这个会在后面任务时一起讲。
    2. 检查线程调度器是否关闭
  3. 检查是否需要脱离中断状态。如果需要跳出循环,否则跳到步骤1。

    1. 检查是否有新的异步任务加入
    2. 检查调度器是否被其他调度器唤醒

源码阅读

以上就是线程调度器大致的调度逻辑。具体的实现细节我们可以结合代码进一步深入。

阅读入口就是运行时的创建的方法。

pub fn create_runtime() -> Runtime {
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();
    runtime
}

显而易见,运行时的创建使用了建造者模式。enable_all()是自定义开启定时器和IO选项。接下来的代码阅读我们都默认开启了定时器和IO选项。既然是建造这模式,核心代码就是build()

pub fn build(&mut self) -> io::Result<Runtime> {
    match &self.kind {
        Kind::CurrentThread => self.build_basic_runtime(),
        #[cfg(feature = "rt-multi-thread")]
        // 多线程运行时
        Kind::MultiThread => self.build_threaded_runtime(),
    }
}

创建的是多线程运行时,所以是self.build_threaded_runtime()

cfg_rt_multi_thread! {
    impl Builder {
        fn build_threaded_runtime(&mut self) -> io::Result<Runtime> {
            use crate::loom::sys::num_cpus;
            use crate::runtime::{Kind, ThreadPool};
            use crate::runtime::park::Parker;
            use std::cmp;

            // 计算线程数量
            let core_threads = self.worker_threads.unwrap_or_else(|| cmp::min(self.max_threads, num_cpus()));
            assert!(core_threads <= self.max_threads, "Core threads number cannot be above max limit");

            // 根据配置新建驱动
            // 这里创建的就是前面讲过的时间驱动和IO驱动。
            let (driver, resources) = driver::Driver::new(self.get_cfg())?;

            // 初始化线程池的数据
            let (scheduler, launch) = ThreadPool::new(core_threads, Parker::new(driver));
            let spawner = Spawner::ThreadPool(scheduler.spawner().clone());

            // Create the blocking pool
            // 顾名思义,这里封装的是线程池相关的阻塞操作。
            // 目前是线程池对于工作线程的管理
            let blocking_pool = blocking::create_blocking_pool(self, self.max_threads);
            let blocking_spawner = blocking_pool.spawner().clone();

            // 封装上述资源的句柄的集合
            let handle = Handle {
                spawner,
                io_handle: resources.io_handle,
                time_handle: resources.time_handle,
                signal_handle: resources.signal_handle,
                clock: resources.clock,
                blocking_spawner,
            };

            // Spawn the thread pool workers
            // 设置上下文
            let _enter = crate::runtime::context::enter(handle.clone());
            // 启动线程池
            launch.launch();

            Ok(Runtime {
                kind: Kind::ThreadPool(scheduler),
                handle,
                blocking_pool,
            })
        }
    }
}

上述代码中核心步骤有如下几个

  1. 创建驱动
  2. 初始化线程池数据
  3. 封装阻塞API
  4. 启动线程池

第一步,创建驱动

前面已经大概讲过这两种驱动。时间驱动是基于时间轮中的一种。关于时间轮的种类和实现有很多。有机会以后补充。这里需要注意的是IO驱动。

TODO 补充IO驱动

第二部,初始化线程池相关数据

impl ThreadPool {
    pub(crate) fn new(size: usize, parker: Parker) -> (ThreadPool, Launch) {
        // 初始化各个子线程数据
        // 其中shared是线程之间的共享数据,launch则是线程自身独享数据
        let (shared, launch) = worker::create(size, parker);
        // 接下来两个步骤都是把线程之间的共享数据包装进线程池
        let spawner = Spawner { shared };
        let thread_pool = ThreadPool { spawner };

        (thread_pool, launch)
    }

}

继续深入worker::create()

pub(super) fn create(size: usize, park: Parker) -> (Arc<Shared>, Launch) {
    let mut cores = vec![];
    let mut remotes = vec![];

    // Create the local queues
    for _ in 0..size {
        // 创建任务队列
        // steal队列中的任务可以被别的调度器窃取
       	// run_queue队列存储的任务是准备被执行的任务
        let (steal, run_queue) = queue::local();

        // 中断
        let park = park.clone();
        let unpark = park.unpark();

        // 线程调度器的私有数据
        cores.push(Box::new(Core {
            tick: 0,
            lifo_slot: None,
            run_queue,
            is_searching: false,
            is_shutdown: false,
            tasks: LinkedList::new(),
            park: Some(park),
            rand: FastRand::new(seed()),
        }));

        // 线程调度器的对外暴露的数据
        remotes.push(Remote {
            steal,
            pending_drop: task::TransferStack::new(),
            unpark,
        });
    }

    // 线程调度器之间的共享数据
    let shared = Arc::new(Shared {
        remotes: remotes.into_boxed_slice(),
        inject: queue::Inject::new(),
        idle: Idle::new(size),
        shutdown_workers: Mutex::new(vec![]),
    });

    // 启动器
    let mut launch = Launch(vec![]);

    for (index, core) in cores.drain(..).enumerate() {
        launch.0.push(Arc::new(Worker {
            shared: shared.clone(),
            index,
            core: AtomicCell::new(Some(core)),
        }));
    }

    (shared, launch)
}

这里先看下线程调度器私有数据的结构。

/// Core data
struct Core {
    /// Used to schedule bookkeeping tasks every so often.
    // 周期计数
    tick: u8,

    /// When a task is scheduled from a worker, it is stored in this slot. The
    /// worker will check this slot for a task **before** checking the run
    /// queue. This effectively results in the **last** scheduled task to be run
    /// next (LIFO). This is an optimization for message passing patterns and
    /// helps to reduce latency.
    // 调度器执行协程时会优先执行这个成员上的任务。计算机遵循“局部性原理”。当我们激活一个协程
    // 时,相关数据仍然在缓存上,这个时候优先执行会带来性能的提升。
    lifo_slot: Option<Notified>,

    /// The worker-local run queue.
    // 任务队列 存放这准备被执行的任务
    run_queue: queue::Local<Arc<Worker>>,

    /// True if the worker is currently searching for more work. Searching
    /// involves attempting to steal from other workers.
    // 是否处于“窃取”
    is_searching: bool,

    /// True if the scheduler is being shutdown
    // 是否关闭
    is_shutdown: bool,

    /// Tasks owned by the core
    // 任务队列 
    tasks: LinkedList<Task, <Task as Link>::Target>,

    /// Parker
    ///
    /// Stored in an `Option` as the parker is added / removed to make the
    /// borrow checker happy.
    // 
    park: Option<Parker>,

    /// Fast random number generator.
    // 快速随机
    rand: FastRand,
}

上面几个重要的成员都加了中文注释。但是还有两个成员之间容易被混淆。接下来讲解下一些重点,

  1. 周期计数tick

    前面讲过为了避免全局异步任务队列中的任务“饥饿”,线程调度器执行一定个数的异步任务后,会尝试去获取全局异步任务队列中的异步任务。这里的周期计数就是为此设计一个计数器。线程调度器每执行一个异步任务(无论来自何处),都会进行加1。当周期达到一个特定数字,线程调度器就会优先去获取全局异步任务队列中的人物。除此之外,当到达一定周期后,异步任务队列还会进行附加的操作,如维护调度器的某些状态,

  2. lifo_slot

    如注释而言,这是根据“局部性原理”设计的成员,用来提高调度器性能。如果有需要,新加入的异步任务可以赋值到这个成员上。而原先上面的异步任务则加入本地异步任务队列中,实现抢占式的调度。

  3. run_queuetasks

    run_queue就是之前一直在讲的本地异步任务队列,用来存放即将执行的异步任务。tasks也是存放异步任务,但有所不同。后面讲解异步任务的结构,我们就可以知道调度器会对提交进来的异步任务进行再包装。包装后的异步任务会把实际的异步任务放入堆中,对外提供一个智能指针和指针计数的功能。线程调度器调度的异步任务也是包装后的异步任务。前面讲过异步任务会与第一次执行所在的线程调度器相互绑定。当调度器持有这个异步任务时,就是把其加入到tasks。这是为了防止异步任务的错误析构。

  4. is_searchingis_shutdown

    这两个很好理解。is_searching在线程调度器尝试“窃取”其他调度器任务时设置,代表searching状态。is_shutdown前面没讲过。这个成员可能在线程调度器维护自身时被修改。它取决于所在线程池调度器是否关闭。

  5. park

    中断处理,对应前面讲的驱动。

  6. rand

    随机生成器。窃取其他调度器的异步任务时,随机从其中一个开始,可以尽量避免数据竞争,提升性能。

接下来是remote。是调度器对其他调度器暴露的数据。

/// Used to communicate with a worker from other threads.
struct Remote {
    /// Steal tasks from this worker.
    // 任务队列 其他调度器可以从这里窃取任务
    steal: queue::Steal<Arc<Worker>>,

    /// Transfers tasks to be released. Any worker pushes tasks, only the owning
    /// worker pops.
    pending_drop: task::TransferStack<Arc<Worker>>,

    /// Unparks the associated worker thread
    unpark: Unparker,
}

现在一样讲解下重要成员。

  1. pending_drop

    回收队列。当线程调度器所持有的异步任务完成需要进行析构时,都会先放入这个队列中。等待调度器维护阶段,再实际清除

  2. unpark

    TODO 等待合适的描述

remote还有一个窃取队列的成员。查看下本地队列和窃取队列的创建方法。

/// Create a new local run-queue
pub(super) fn local<T: 'static>() -> (Steal<T>, Local<T>) {
    let mut buffer = Vec::with_capacity(LOCAL_QUEUE_CAPACITY);

    for _ in 0..LOCAL_QUEUE_CAPACITY {
        buffer.push(UnsafeCell::new(MaybeUninit::uninit()));
    }

    let inner = Arc::new(Inner {
        head: AtomicU32::new(0),
        tail: AtomicU16::new(0),
        buffer: buffer.into(),
    });

    let local = Local {
        inner: inner.clone(),
    };

    let remote = Steal(inner);

    (remote, local)
}

上面代码可以看出,窃取队列和本地队列的内部队列其实是同一个。队列所属的线程调度器与其他线程调度器对任务的窃取形成了数据竞争。这里讲解下,tokio如何解决队列的数据竞争问题。这也是窃取的核心操作。

首先,队列插入异步任务并不是并发的。插入操作永远只有队列所属的线程可以进行。存在并发的是本地队列的消费和其他调度器对它的窃取。

可以看到内部队列的head指针是一个Atomic32,,而tail指针是一个Atomic16。二者宽度不同。head指针需要拆成两个16位的指针stealreadlly_head。这两个指针可能相等,也可能不等。窃取任务一般都是批量窃取的。

  1. 两个指针相等时。这是两者都代表真正的头部指针。```pop``出任务时,只需要获取它们所指向的任务。
  2. 两个指针不等时。这个时候表明有其他调度器窃取本地异步任务。这个时候,steal代表窃取的批量指针的头部,而really_head指向的是窃取的批量任务的尾部指针的下一个任务。

窃取操作一般会尝试去设置head指针。如果解析head指针出来的stealreally_head指针不等,说明除它之外,还有其他调度器试图窃取这个队列的任务。那么放弃窃取。 即不存在并发窃取。相等的话,则会去计算新的头部指针。伪代码如下:

// unpack是分离操作。将一个32位指针分割成前后两个16位指针
// 这个时候steal等于really_head
let (steal,really_head)=unpack(head);
// 计算出需要窃取的个数
let n=... ;
// 计算出新的head
let new_head=pack(steal,steal+n);

一旦设置新的头部指针成功,就可以把从指针stealsteal+n-1的异步任务窃取过来。窃取过来后,再设置头部指针位pack(steal+n,steal_n)。表示窃取完成。

最后是Share是所有调度器共享的数据。Remote数据是一个调度器向其他调度器暴露的数据。所以也包含其中

/// State shared across all workers
pub(super) struct Shared {
    /// Per-worker remote state. All other workers have access to this and is
    /// how they communicate between each other.
    // 线程调度器对外暴露的数据
    remotes: Box<[Remote]>,

    /// Submit work to the scheduler while **not** currently on a worker thread.
    // 任务队列
    inject: queue::Inject<Arc<Worker>>,

    /// Coordinates idle workers
    // 空闲状态的线程
    idle: Idle,

    /// Workers have have observed the shutdown signal
    ///
    /// The core is **not** placed back in the worker to avoid it from being
    /// stolen by a thread that was spawned as part of `block_in_place`.
    shutdown_workers: Mutex<Vec<(Box<Core>, Arc<Worker>)>>,
}

继续讲解成员

  1. inject

    前面提过的全局异步任务队列

  2. idle

    用来追踪处于searchingpark状态的线程调度器。定义如下

    pub(super) struct Idle {
        /// Tracks both the number of searching workers and the number of unparked
        /// workers.
        ///
        /// Used as a fast-path to avoid acquiring the lock when needed.
        state: AtomicUsize,
    
        /// Sleeping workers
        /// 存放沉睡的调度器序号,也就是处于Park(中断)状态的线程调度器
        sleepers: Mutex<Vec<usize>>,
    
        /// Total number of workers.
        /// 线程池调度器总的子线程数量
        num_workers: usize,
    }
    
  3. shutdown_workers

    记录关闭的线程调度器。用来存放被关闭的线程调度器。

第三步,初始化阻塞API和数据

与前面的数据不同,这里初始化的是线程部分的数据和封装了相关的线程操作,包括线程池中线程的创建和运行。

pub(crate) struct BlockingPool {
    spawner: Spawner,
    shutdown_rx: shutdown::Receiver,
}

#[derive(Clone)]
pub(crate) struct Spawner {
    inner: Arc<Inner>,
}

struct Inner {
    /// State shared between worker threads
    shared: Mutex<Shared>,

    /// Pool threads wait on this.
    condvar: Condvar,

    /// Spawned threads use this name
    thread_name: ThreadNameFn,

    /// Spawned thread stack size
    stack_size: Option<usize>,

    /// Call after a thread starts
    after_start: Option<Callback>,

    /// Call before a thread stops
    before_stop: Option<Callback>,

    // Maximum number of threads
    thread_cap: usize,

    // Customizable wait timeout
    // 线程如果分配到一个线程调度器,存活多久
    keep_alive: Duration,
}

struct Shared {
    // 任务队列
    queue: VecDeque<Task>,
    num_th: usize,
    num_idle: u32,
    num_notify: u32,
    shutdown: bool,
    shutdown_tx: Option<shutdown::Sender>,
    /// Prior to shutdown, we clean up JoinHandles by having each timed-out
    /// thread join on the previous timed-out thread. This is not strictly
    /// necessary but helps avoid Valgrind false positives, see
    /// https://github.com/tokio-rs/tokio/commit/646fbae76535e397ef79dbcaacb945d4c829f666
    /// for more information.
    last_exiting_thread: Option<thread::JoinHandle<()>>,
    /// This holds the JoinHandles for all running threads; on shutdown, the thread
    /// calling shutdown handles joining on these.
    worker_threads: HashMap<usize, thread::JoinHandle<()>>,
    /// This is a counter used to iterate worker_threads in a consistent order (for loom's
    /// benefit)
    worker_thread_index: usize,
}

可以看到,BlockingPool主要封装了线程池管理线程所需要的一些数据,如创建工作线程时的线程名字,栈的大小,空间时能够存活的时间等等。继续查看,还会它封装了相关的API。这里需要注意的是Shared的成员queue。它也是一个任务队列,但是与前面不同,它是一个线程任务队列。运行时会在里面插入复数个线程调度器的启动方法,与线程数量一致。每一个工作线程都会分配到一个,并运行一个线程调度器。

第四步,启动运行时

impl Launch {
    pub(crate) fn launch(mut self) {
        for worker in self.0.drain(..) {
            runtime::spawn_blocking(move || run(worker));
        }
    }
}

对应调度器数量n,创建n个线程,每一个线程运行调度器的相关逻辑。spawn_blocking逻辑是创建线程,而run(worker)的逻辑是调度器运行逻辑。现在先查看线程创建和运行逻辑。

#[cfg_attr(tokio_track_caller, track_caller)]
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
where
    F: FnOnce() -> R + Send + 'static,
    R: Send + 'static,
{
    #[cfg(feature = "tracing")]
    let func = {
        // 省略部分是tracing逻辑。tracing是一个优秀的日志框架
        ... ...
        move || {
            ... ...
            func()
        }
    };
    
    // 包装业务逻辑成一个任务结构
    let (task, handle) = task::joinable(BlockingTask::new(func));
    // 前面知道BlockingPool的相关API有关于线程池管理线程的
    // 这里就是创建一个线程来执行任务
    let _ = self.blocking_spawner.spawn(task, &self);
    handle
}

TODO 之后会详细讲解任务结构的包装

这里有两个重要的步骤。一个是task::joinable会包装一个任务结构;一个是self.blocking_spawner.spawn会创建一个线程来执行这个任务。任务结构的包装在后面讲。现在先看下线程的创建过程。

pub(crate) fn spawn(&self, task: Task, rt: &Handle) -> Result<(), ()> {
    let shutdown_tx = {
        let mut shared = self.inner.shared.lock();

        // 判断线程池是否关闭
        if shared.shutdown {
            // Shutdown the task
            task.shutdown();

            // no need to even push this task; it would never get picked up
            return Err(());
        }

        // 把任务加入线程池的任务队列
        shared.queue.push_back(task);

        if shared.num_idle == 0 {
            // No threads are able to process the task.

            if shared.num_th == self.inner.thread_cap {
                // At max number of threads
                None
            } else {
                shared.num_th += 1;
                assert!(shared.shutdown_tx.is_some());
                shared.shutdown_tx.clone()
            }
        } else {
            // Notify an idle worker thread. The notification counter
            // is used to count the needed amount of notifications
            // exactly. Thread libraries may generate spurious
            // wakeups, this counter is used to keep us in a
            // consistent state.
            shared.num_idle -= 1;
            shared.num_notify += 1;
            self.inner.condvar.notify_one();
            None
        }
    };

    if let Some(shutdown_tx) = shutdown_tx {
        let mut shared = self.inner.shared.lock();

        let id = shared.worker_thread_index;
        shared.worker_thread_index += 1;

        let handle = self.spawn_thread(shutdown_tx, rt, id);

        shared.worker_threads.insert(id, handle);
    }

    Ok(())
}

之前讲过将线程调度器任务放入任务队列中,每个工作线程获取并运行一个线程调度器。上述代码就是描述这一过程。大概逻辑如下:

  1. 判断线程池是否关闭。如果线程池关闭了,任务取消,返回。否则继续。
  2. 把任务加入到线程池的任务队列等待执行。
  3. 判断是否有空闲的线程。如果没有,步骤4,否则步骤5。
  4. 查看目前的线程数量是否为线程池允许的最大线程数量。如果是返回;否则步骤6。
  5. 唤醒空闲的睡眠线程。返回。
  6. 创建新的线程。

创建新的线程的逻辑如下

fn spawn_thread(
    &self,
    shutdown_tx: shutdown::Sender,
    rt: &Handle,
    id: usize,
) -> thread::JoinHandle<()> {
    let mut builder = thread::Builder::new().name((self.inner.thread_name)());

    if let Some(stack_size) = self.inner.stack_size {
        builder = builder.stack_size(stack_size);
    }

    // 运行时的句柄
    let rt = rt.clone();

    builder
        .spawn(move || {
            // Only the reference should be moved into the closure
            // 调度器设置运行时的上下文
            let _enter = crate::runtime::context::enter(rt.clone());
           // 工作线程运行逻辑
            rt.blocking_spawner.inner.run(id);
            // 线程关闭逻辑
            drop(shutdown_tx);
        })
        .unwrap()
}

TODO 运行时上下文设置前面已经说过了

工作线程运行逻辑如下

fn run(&self, worker_thread_id: usize) {
    if let Some(f) = &self.after_start {
        f()
    }

    // 线程获取线程池资源是存在数据竞争的 加锁
    let mut shared = self.shared.lock();
    let mut join_on_thread = None;

    'main: loop {
        // BUSY
        // 从线程池的队列中获取任务
        // 这里的任务就是线程调度器的运行逻辑
        while let Some(task) = shared.queue.pop_front() {
            // 释放锁
            drop(shared);
            // 运行任务 这里的任务是调度器的相关逻辑
            task.run();

            // 再次获取线程池队列中的任务需要重新上锁
            shared = self.shared.lock();
        }

        // IDLE
        // 调度器发现没有需要运行的协程,就会结束。
        // 如果这个时候没有启动新的线程调度器需要,线程就会跑到这里,进入空闲状态
        shared.num_idle += 1;

        // 如果线程池还没关闭
        while !shared.shutdown {
            // 线程睡眠一段时间
            let lock_result = self.condvar.wait_timeout(shared, self.keep_alive).unwrap();

           	// 跑到这里说明线程被唤醒了,可能是被其他线程唤醒也可能睡眠的时间过了
            shared = lock_result.0;
            let timeout_result = lock_result.1;

            
            if shared.num_notify != 0 {
                // We have received a legitimate wakeup,
                // acknowledge it by decrementing the counter
                // and transition to the BUSY state.
                // num_notify仅在其他线程唤醒时加1。
                // 检查到这个成员不为0说明有唤醒沉睡线程的需求
                shared.num_notify -= 1;
                break;
            }

            // Even if the condvar "timed out", if the pool is entering the
            // shutdown phase, we want to perform the cleanup logic.
            // 如果是沉睡超时,则线程池移除该线程
            if !shared.shutdown && timeout_result.timed_out() {
                // We'll join the prior timed-out thread's JoinHandle after dropping the lock.
                // This isn't done when shutting down, because the thread calling shutdown will
                // handle joining everything.
                let my_handle = shared.worker_threads.remove(&worker_thread_id);
                // 拿到上一个准备结束的线程的句柄
                join_on_thread = std::mem::replace(&mut shared.last_exiting_thread, my_handle);

                break 'main;
            }

            // Spurious wakeup detected, go back to sleep.
        }

        // 线程池关闭
        if shared.shutdown {
            // Drain the queue
            while let Some(task) = shared.queue.pop_front() {
                drop(shared);
                task.shutdown();

                shared = self.shared.lock();
            }

            // Work was produced, and we "took" it (by decrementing num_notify).
            // This means that num_idle was decremented once for our wakeup.
            // But, since we are exiting, we need to "undo" that, as we'll stay idle.
            shared.num_idle += 1;
            // NOTE: Technically we should also do num_notify++ and notify again,
            // but since we're shutting down anyway, that won't be necessary.
            break;
        }
    }

    // 接下来是逻辑是线程被移除出线程后的逻辑
    
    // Thread exit
    // 线程池数量减1
    shared.num_th -= 1;

    // num_idle should now be tracked exactly, panic
    // with a descriptive message if it is not the
    // case.
    // 线程吃空闲的线程+1
    shared.num_idle = shared
        .num_idle
        .checked_sub(1)
        .expect("num_idle underflowed on thread exit");

    // TODO 这里不懂
    if shared.shutdown && shared.num_th == 0 {
        self.condvar.notify_one();
    }

    drop(shared);

    if let Some(f) = &self.before_stop {
        f()
    }

    if let Some(handle) = join_on_thread {
        let _ = handle.join();
    }
}

上述逻辑比较简单,唯一比较复杂只有线程沉睡和唤醒部分。

fn run(&self, worker_thread_id: usize) {
    
    ... ...
    
    'main: loop {
   		... ...

        // 如果线程池还没关闭
        while !shared.shutdown {
            // 线程睡眠一段时间
            let lock_result = self.condvar.wait_timeout(shared, self.keep_alive).unwrap();

           	// 跑到这里说明线程被唤醒了,可能是被其他线程唤醒也可能睡眠的时间过了
            shared = lock_result.0;
            let timeout_result = lock_result.1;

            // 检查是不是其他线程唤醒的
            if shared.num_notify != 0 {
                shared.num_notify -= 1;
                break;
            }

            // 再次检查了是不是其他线程唤醒的
            if !shared.shutdown && timeout_result.timed_out() {
                ... ...
            }

            // Spurious wakeup detected, go back to sleep.
        }

		... ...
    }
	
    ... ...
}

前面代码可以知道num_notify在唤醒线程时会加1。因此如果检查到这个成员不为0,线程如果被在唤醒后检查到这个成员不为0,就可以任务是正常唤醒的。之所以接下来还需要检查唤醒结果是正常唤醒还是超时苏醒,是为了防止出现虚拟唤醒。

note 虚拟唤醒是指多处理器下,线程A在调用API唤醒一个其他因条件变量沉睡的线程时,出现多个线程被唤醒。除了第一个,其他唤醒的称为“虚拟唤醒”。这是底层原语实现造成。尽管底层原语可以实现时进行修复,但是虚拟唤醒的出现概率很低。为此进行修改而导致效率降低不值得,一般都是业务层做处理。

启动线程调度器,接下来是调度器的运行逻辑。

fn run(worker: Arc<Worker>) {
    // Acquire a core. If this fails, then another thread is running this
    // worker and there is nothing further to do.
    let core = match worker.core.take() {
        Some(core) => core,
        None => return,
    };

    // Set the worker context.
    let cx = Context {
        worker,
        core: RefCell::new(None),
    };

    let _enter = crate::runtime::enter(true);

    CURRENT.set(&cx, || {
        // This should always be an error. It only returns a `Result` to support
        // using `?` to short circuit.
        assert!(cx.run(core).is_err());
    });
}


cx.run(core)的源码如下

fn run(&self, mut core: Box<Core>) -> RunResult {
    while !core.is_shutdown {
        // Increment the tick
        // 周期计数
        core.tick();

        // Run maintenance, if needed
        // 维持调度器
        core = self.maintenance(core);

        // First, check work available to the current worker.
        // 提取任务并执行
        if let Some(task) = core.next_task(&self.worker) {
            core = self.run_task(task, core)?;
            continue;
        }

        // There is no more **local** work to process, try to steal work
        // from other workers.
        // 如果调度器提取不到任务就会试图去窃取其他繁忙的调度器的任务
        if let Some(task) = core.steal_work(&self.worker) {
            core = self.run_task(task, core)?;
        } else {
            // Wait for work
            core = self.park(core);
        }
    }

    // Signal shutdown
    self.worker.shared.shutdown(core, self.worker.clone());
    Err(())
}

上述代码框架之前已经讲过。因此获取下一个任务不重复讲述。现在深入一些细节。

第一个就是调度器的维护

 core = self.maintenance(core);

maintenance(core)源码如下

fn maintenance(&self, mut core: Box<Core>) -> Box<Core> {
    if core.tick % GLOBAL_POLL_INTERVAL == 0 {
        // Call `park` with a 0 timeout. This enables the I/O driver, timer, ...
        // to run without actually putting the thread to sleep.
        // 处理中断
        core = self.park_timeout(core, Some(Duration::from_millis(0)));

        // Run regularly scheduled maintenance
        // 维护
        core.maintenance(&self.worker);
    }

    core
}

core.maintenance(&self.worker);的源码如下

/// Runs maintenance work such as free pending tasks and check the pool's
/// state.
fn maintenance(&mut self, worker: &Worker) {
    // 析构完成的任务
    self.drain_pending_drop(worker);

    if !self.is_shutdown {
        // Check if the scheduler has been shutdown
        // 这里检查线程池调度器是否关闭
        self.is_shutdown = worker.inject().is_closed();
    }
}

总结上面线程调度器维护自身的逻辑

  1. 处理中断
  2. 维护核core数据
    1. 析构完成的任务
    2. 检查是否关闭

第二个就是窃取任务

fn steal_work(&mut self, worker: &Worker) -> Option<Notified> {
    // 转化为"searching"状态
    if !self.transition_to_searching(worker) {
        return None;
    }

    let num = worker.shared.remotes.len();
    // Start from a random worker
    // 从一个随机线程调度器开始
    let start = self.rand.fastrand_n(num as u32) as usize;

    for i in 0..num {
        let i = (start + i) % num;

        // Don't steal from ourself! We know we don't have work.
        if i == worker.index {
            continue;
        }

        let target = &worker.shared.remotes[i];
        // 窃取开始
        if let Some(task) = target.steal.steal_into(&mut self.run_queue) {
            return Some(task);
        }
    }

    // Fallback on checking the global queue
    worker.shared.inject.pop()
}

具体的窃取逻辑如下。窃取的核心思路之前已经讲解过了。

impl<T> Steal<T> {

    /// Steals half the tasks from self and place them into `dst`.
    pub(super) fn steal_into(&self, dst: &mut Local<T>) -> Option<task::Notified<T>> {
        // Safety: the caller is the only thread that mutates `dst.tail` and
        // holds a mutable reference.
        let dst_tail = unsafe { dst.inner.tail.unsync_load() };

        // To the caller, `dst` may **look** empty but still have values
        // contained in the buffer. If another thread is concurrently stealing
        // from `dst` there may not be enough capacity to steal.
        let (steal, _) = unpack(dst.inner.head.load(Acquire));

        if dst_tail.wrapping_sub(steal) > LOCAL_QUEUE_CAPACITY as u16 / 2 {
            // we *could* try to steal less here, but for simplicity, we're just
            // going to abort.
            return None;
        }

        // Steal the tasks into `dst`'s buffer. This does not yet expose the
        // tasks in `dst`.
        let mut n = self.steal_into2(dst, dst_tail);

        if n == 0 {
            // No tasks were stolen
            return None;
        }

        // We are returning a task here
        n -= 1;

        let ret_pos = dst_tail.wrapping_add(n);
        let ret_idx = ret_pos as usize & MASK;

        // safety: the value was written as part of `steal_into2` and not
        // exposed to stealers, so no other thread can access it.
        let ret = dst.inner.buffer[ret_idx].with(|ptr| unsafe { ptr::read((*ptr).as_ptr()) });

        if n == 0 {
            // The `dst` queue is empty, but a single task was stolen
            return Some(ret);
        }

        // Make the stolen items available to consumers
        dst.inner.tail.store(dst_tail.wrapping_add(n), Release);

        Some(ret)
    }

    // Steal tasks from `self`, placing them into `dst`. Returns the number of
    // tasks that were stolen.
    fn steal_into2(&self, dst: &mut Local<T>, dst_tail: u16) -> u16 {
        let mut prev_packed = self.0.head.load(Acquire);
        let mut next_packed;

        let n = loop {
            let (src_head_steal, src_head_real) = unpack(prev_packed);
            let src_tail = self.0.tail.load(Acquire);

            // If these two do not match, another thread is concurrently
            // stealing from the queue.
            if src_head_steal != src_head_real {
                return 0;
            }

            // Number of available tasks to steal
            let n = src_tail.wrapping_sub(src_head_real);
            let n = n - n / 2;

            if n == 0 {
                // No tasks available to steal
                return 0;
            }

            // Update the real head index to acquire the tasks.
            let steal_to = src_head_real.wrapping_add(n);
            assert_ne!(src_head_steal, steal_to);
            next_packed = pack(src_head_steal, steal_to);

            // Claim all those tasks. This is done by incrementing the "real"
            // head but not the steal. By doing this, no other thread is able to
            // steal from this queue until the current thread completes.
            let res = self
                .0
                .head
                .compare_exchange(prev_packed, next_packed, AcqRel, Acquire);

            match res {
                Ok(_) => break n,
                Err(actual) => prev_packed = actual,
            }
        };

        assert!(n <= LOCAL_QUEUE_CAPACITY as u16 / 2, "actual = {}", n);

        let (first, _) = unpack(next_packed);

        // Take all the tasks
        for i in 0..n {
            // Compute the positions
            let src_pos = first.wrapping_add(i);
            let dst_pos = dst_tail.wrapping_add(i);

            // Map to slots
            let src_idx = src_pos as usize & MASK;
            let dst_idx = dst_pos as usize & MASK;

            // Read the task
            //
            // safety: We acquired the task with the atomic exchange above.
            let task = self.0.buffer[src_idx].with(|ptr| unsafe { ptr::read((*ptr).as_ptr()) });

            // Write the task to the new slot
            //
            // safety: `dst` queue is empty and we are the only producer to
            // this queue.
            dst.inner.buffer[dst_idx]
                .with_mut(|ptr| unsafe { ptr::write((*ptr).as_mut_ptr(), task) });
        }

        let mut prev_packed = next_packed;

        // Update `src_head_steal` to match `src_head_real` signalling that the
        // stealing routine is complete.
        loop {
            let head = unpack(prev_packed).1;
            next_packed = pack(head, head);

            let res = self
                .0
                .head
                .compare_exchange(prev_packed, next_packed, AcqRel, Acquire);

            match res {
                Ok(_) => return n,
                Err(actual) => {
                    let (actual_steal, actual_real) = unpack(actual);

                    assert_ne!(actual_steal, actual_real);

                    prev_packed = actual;
                }
            }
        }
    }
}

至此运行时有关调度机制部分大致讲解完毕。另外一部分就是有关任务部分,将另起一篇讲述。

上一篇:网站搭建流程


下一篇:多媒体技术基础概念-计算题 小k和大K 小b和大B