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,执行一个
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().await | tokio 调度器:当前 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一次就解开。