在 macOS 上把系统时间往回拨,try_lock_for 多等了一大截
主线位置:主线 1 的超时分支落到平台层。
try_lock_for→lock_slow(Some(deadline))→park(.., timeout)→ThreadParker::park_until;macOS 上park_until用 pthread 条件变量,而 Apple 平台不能把条件变量设成单调时钟(ThreadParker 的各平台实现)。
flowchart TB
top["park_until(deadline):deadline 是单调时钟 Instant"] --> chk{"Instant::now() 已过 deadline?"}
chk -->|"是"| out["返回 false:超时"]
chk -->|"否"| ts["绝对时间 = gettimeofday() + 剩余时长"]
ts --> tw["pthread_cond_timedwait(绝对时间)"]
tw -->|"被叫醒或到点"| loopq{"should_park 还为真?"}
loopq -->|"是"| chk
loopq -->|"否"| ok["返回 true"]
tw -.->|"睡眠中墙上时钟往回拨 X"| late["这次 timedwait 多睡 X"]
tw -.->|"睡眠中往前拨"| early["提前返回,但回到 chk 用单调时钟复查,接着睡"]
现象(从代码推出)
在 macOS(及其他 Apple 平台)上,一个带超时的等待——Mutex::try_lock_for、RwLock::try_write_for、Condvar::wait_for 等——在等待期间系统时间被往回调整(手动改时间、NTP 大步校正),这次等待会比给定的超时多等「回拨的长度」。往前调整则不影响:不会提前超时。
本页没有实测:要复现得修改本机系统时间,本卷不做。下面的结论全部来自读代码。
源码出处
core/src/thread_parker/unix.rs 的 park_until:
unsafe fn park_until(&self, timeout: Instant) -> bool {
let r = libc::pthread_mutex_lock(self.mutex.get());
debug_assert_eq!(r, 0);
while self.should_park.get() {
let now = Instant::now();
if timeout <= now {
let r = libc::pthread_mutex_unlock(self.mutex.get());
debug_assert_eq!(r, 0);
return false;
}
if let Some(ts) = timeout_to_timespec(timeout - now) {
let r = libc::pthread_cond_timedwait(self.condvar.get(), self.mutex.get(), &ts);
// ...
} else {
// Timeout calculation overflowed, just sleep indefinitely
let r = libc::pthread_cond_wait(self.condvar.get(), self.mutex.get());
// ...
}
}
// ...
true
}
timeout_to_timespec把剩余时长加到timespec_now()上。Apple 平台上timespec_now()是gettimeofday(185-198 行),因为这些平台上init不设CLOCK_MONOTONIC(128-148 行,没有pthread_condattr_setclock)。- 交给内核的是墙上时钟的绝对时间点。睡眠期间墙上时钟往回拨 X,这个时间点在墙上时钟里就远了 X,这一次
pthread_cond_timedwait要多睡 X 才返回。 - 往前拨时
pthread_cond_timedwait会提前返回ETIMEDOUT,但循环开头用单调时钟Instant::now()重新判断,没到期就按新的剩余时长再睡一次。所以不会提前结束。 - Linux 等平台在
init里把条件变量设成CLOCK_MONOTONIC;Linux 实际走的是 futex 后端(相对超时),都不受影响。
0.13 改了:CHANGELOG「Made timed waits on Apple platforms independent of wall-clock changes. (#533)」,提交 c175220「Use relative timed waits on Apple platforms」,改用 pthread_cond_timedwait_relative_np(去掉的与修正的)。
怎么改
- 不依赖超时等待的精确上界做正确性判断(例如「超时就认为对方死了」);超时只当作「大概多久后放弃」。
- 需要在 Apple 平台上严格的上界:升级到 0.13,或者把长超时拆成短超时的循环(每次几十毫秒,循环里自己用
Instant判断总时长),把回拨的影响限制在一次短等待之内。 - 生产服务器是 Linux 的话,这一条不影响线上;只影响 macOS 上的开发、桌面应用和 iOS。
小结
- 0.12.5 在 Apple 平台上把超时换算成
gettimeofday的绝对时间交给pthread_cond_timedwait。 - 墙上时钟往回拨会让那一次等待多等回拨的长度;往前拨会被单调时钟的复查兜住,不会提前超时。
- 只读代码,没有改本机时间实测。
- 0.13 改为相对等待;0.12.5 上要严格上界就拆成短超时循环。
💬 关联讨论
发起讨论加载中...