Rust in Everything
基于 rayon-core 1.13.0

yield_now 与 yield_local

主线位置:一次 join 里 wait_until 的手动版。join 等 B 时会自动去找别的活干;用户代码在 rayon 任务里等一个条件时,yield_now 让它也能这么做:找一个任务,在当前栈上执行,然后回来。

flowchart LR
  y["rayon::yield_now()"] --> cur{"当前线程是 worker?"}
  cur -->|"否"| none["None"]
  cur -->|"是"| fw["find_work:本地 deque → 广播队列 → 偷别人 → 注入队列"]
  fw -->|"找到"| exec["在当前栈上执行,返回 Some(Executed)"]
  fw -->|"没找到"| idle["Some(Idle),不睡、不让出 OS 线程"]
  yl["rayon::yield_local()"] --> tl["take_local_job:本地 deque → 广播队列"]
  tl --> exec
  tl --> idle

rayon 1.7.0 / rayon-core 1.11.0 加入(9fa93e0,2023-02,合并于 #1026)。

实现:find_work 或 take_local_job,执行一个

registry.rs 第 844–862 行:

pub(super) fn yield_now(&self) -> Yield {
    match self.find_work() {
        Some(job) => unsafe {
            self.execute(job);
            Yield::Executed
        },
        None => Yield::Idle,
    }
}

pub(super) fn yield_local(&self) -> Yield {
    match self.take_local_job() {
        Some(job) => unsafe {
            self.execute(job);
            Yield::Executed
        },
        None => Yield::Idle,
    }
}

对外的函数在 thread_pool/mod.rs 第 459–502 行:

pub fn yield_now() -> Option<Yield> {
    unsafe {
        let thread = WorkerThread::current().as_ref()?;
        Some(thread.yield_now())
    }
}

pub enum Yield {
    /// Work was found and executed.
    Executed,
    /// No available work was found.
    Idle,
}
  • 不在任何池的 worker 上,返回 None,什么也不做。ThreadPool::yield_now / yield_local 还要求当前线程属于这个池。
  • yield_now 用的是主循环的 find_work:本地 deque、本线程的广播队列、随机起点偷别的线程、全局注入队列。yield_local 只做前两步,不偷也不看注入队列。
  • 一次只执行一个任务。那个任务内部可以再 join、再偷,完成后才返回。
  • 找不到活时返回 Idle,不睡,也不调 std::thread::yield_now。不碰 sleep 模块的计数器(不 start_looking、不 work_found)。文档的建议是:写轮询循环时,rayon 没有活就自己再让给 OS。

和另外两个 yield_now 的区别

让给谁当前代码的状态
std::thread::yield_now()OS 调度器:当前线程让出 CPU,可能马上又被调度回来线程原地不动,什么也不执行
tokio::task::yield_now().awaittokio 调度器:当前 async 任务挂起,让同一个 worker 上的其他任务先跑当前任务的状态保存在 future 里,栈被释放
rayon::yield_now()rayon:在当前线程上执行一个别的 rayon 任务当前代码的栈帧还在,别的任务压在它上面执行,执行完才返回

第三种和 join 里的 wait_until 是同一回事,所以也继承了同样的代价:被执行的任务跑在当前栈帧之上,当前代码持有的锁、RefCell 借用、线程本地状态它都「看得见」。join 一章里的持锁死锁(E7)换成 yield_now 一样会发生。在一个被 yield_now 执行的任务里再调 yield_now,栈会一层层叠上去(推测:代码里没有深度限制)。

用途:在 rayon 任务里等另一个 rayon 任务

join 的文档警告过:一个闭包阻塞等另一个闭包(比如用 channel)可能死锁。原因是等待的那个任务占着 worker,而它等的任务可能就在同一个 worker 的 deque 里排在后面。实验 E16(exp-rayon/yield-poll/)把这个情况做成确定性的:1 线程的池,scope 里先 spawn 一个「置标志」的任务,再 spawn 一个「等标志」的任务。唯一的 worker 按 LIFO 先弹出等待者,置标志的任务压在它下面。等待者每轮做三种事之一:

等待方式结果(各 3 次)
std::hint::spin_loop()3 秒看门狗超时,卡死
rayon::yield_now()第 1 轮就返回 Executed(执行了置标志的任务),随即结束
rayon::yield_local()同上(置标志的任务在本地 deque 里)

换成阻塞的 recv() 或 Condvar::wait 也一样卡死:OS 线程睡了,rayon 不会再往这个 worker 上派活。池子大一点时,只要别的线程都忙,或者被等的任务恰好排在同一个 worker 的 deque 里,问题就会出现,只是变成了概率性的。broadcast 一章的实验 E15 里,长任务每 1 ms 调一次 yield_local(),广播任务就不必等它结束。

轮询写法:

while !ready.load(Ordering::Acquire) {
    match rayon::yield_now() {
        Some(rayon::Yield::Executed) => {}                       // 干了点活,再看条件
        Some(rayon::Yield::Idle) | None => std::thread::yield_now(), // 池里没活,让给 OS
    }
}

能不等就不等更好:把「等另一个任务的结果」改写成 join 或 scope,由 rayon 的 latch 来等。

current_thread_has_pending_tasks

同一个文件里还有一个相关的查询(第 451–457 行):current_thread_has_pending_tasks() 返回 Some(!worker.local_deque_is_empty()),只看本地 deque(不含广播队列)。可以用来决定要不要 yield_local,或者判断当前任务是不是已经切得足够细。

小结

  • yield_now = 执行一个 find_work 找到的任务;yield_local = 执行一个 take_local_job 找到的任务(本地 deque 或广播队列)。不在 worker 上返回 None,找不到返回 Idle,不睡也不让出 OS 线程。
  • 和 std、tokio 的 yield_now 不同,它在当前栈帧上执行别的任务,带着 wait_until 的全部代价(持锁、RefCell、TLS)。
  • 在 rayon 任务里等另一个 rayon 任务时,自旋或阻塞可能卡死(E16:1 线程池上 3/3 卡死),轮询 yield_now 一次就解开。

💬 关联讨论

发起讨论
加载中...