Rust in Everything
基于 parking_lot 0.12.5

在 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 上要严格上界就拆成短超时循环。

💬 关联讨论

发起讨论
加载中...