ConditionVariable&Semaphore与CLOCK_MONOTONIC

背景知识

CLOCK_MONOTONIC和CLOCK_REALTIME

https://linux.die.net/man/3/clock_gettime

Linux操作系统提供了多种时钟源,其中最常用的两个是:

  • CLOCK_REALTIME,系统时钟,一般会和时间服务器进行时间同步,所以此时钟并不是单调递增的,可能会发生回退,也可能往前跳
  • CLOCK_MONOTONIC,单调时钟,不可以被设置、修改,从某个时间点开始(一般是开机)根据晶振进行计数,相当于是一个计数器,所以它不会发生回退和向前的跳变

对应到C++中的就是std::system_clock和std::steady_clock。

ConditionVariable和Semaphore

ConditionVariable和Semaphore都是线程间同步的一种方式,Linux系统提供了POSIX的接口,C++也提供了相应的封装:std::conditionvariable和std::semaphore(引入的比较晚),而它们都提供了类似wait_for/wait_until的功能(Semaphore的是try_acquire_for/until),这个时候它们就和时钟产生了关联。

问题现象

在业务代码中,用了ConditionVariable和Semaphore做了两个事情:

  • 使用ConditionVariable实现了一个定时器,超时的时候做一些其他的工作
  • Semaphore,用于进程间的同步

实际使用中,发现当系统时间发生回退时,ConditionVariable和Semaphore都无法被唤醒,直到系统时间恢复正常。

原因分析

ConditionVariable

由于使用的是C++17,所以直接使用的std::condition_variable,查看它的说明文档,它提供了system_clock和steady_clock的time_point的接口:https://en.cppreference.com/w/cpp/thread/condition_variable/wait_until

template< class Clock, class Duration >
std::cv_status
    wait_until( std::unique_lock<std::mutex>& lock,
                const std::chrono::time_point<Clock, Duration>& abs_time );

template< class Clock, class Duration, class Predicate >
bool wait_until( std::unique_lock<std::mutex>& lock,
                 const std::chrono::time_point<Clock, Duration>& abs_time,
                 Predicate pred );

如果采用system_clock,当system_clock跳变时确实可能会产生虚假的唤醒或者无法唤醒的情况,那如果采用steadyclock是不是就可以了呢,实际测试发现即使使用了steadyclock,还是同样的情况,说明std::condition_variable并没有真正使用monotonic clock,官网也明确写了这个事情:

The standard recommends that the clock tied to abs_time be used to measure time; that clock is not required to be a monotonic clock. There are no guarantees regarding the behavior of this function if the clock is adjusted discontinuously, but the existing implementations convert abs_time from Clock to std::chrono::system_clock and delegate to POSIX pthread_cond_timedwait so that the wait honors adjustments to the system clock, but not to the user-provided Clock.

Even if the clock in use is std::chrono::steady_clock or another monotonic clock, a system clock adjustment may induce a spurious wakeup.

Semaphore

Semaphore是C++20才引入,所以业务代码中直接使用的Linux提供的POSIX的sem接口,semaphore同样提供了类似超时等待的函数:https://man7.org/linux/man-pages/man3/sem_wait.3.html

查看sem_timedwait的API说明:https://linux.die.net/man/3/sem_wait

sem_timedwait() is the same as sem_wait(), except that abs_timeout specifies a limit on the amount of time that the call should block if the decrement cannot be immediately performed. The abs_timeout argument points to a structure that specifies an absolute timeout in seconds and nanoseconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC).

所以看到sem_timedwait也是使用的系统时钟,所以当系统时间跳变时,Semaphore也会出现虚假唤醒或无法唤醒的情况。

解决方案

ConditionVariable—使用pthread接口重新实现

经过Google发现Linux的pthread接口是提供Monotonic clock接口的:https://stackoverflow.com/questions/46219838/pthread-cond-timedwait-for-periodic-tasks-wall-clock-changes,所以只需要自己通过pthread实现一个ConditionVariable就可以解决时间跳变引发的虚假唤醒和无法唤醒的问题。

Example:

    pthread_condattr_t cattr;
    errno = pthread_condattr_init (&cattr);                                                              
    CHECK(errno == 0) << (perror("pthread_condattr_init"), "init");     
    // 指定是CLOCK_MONOTONIC                                 
    errno = pthread_condattr_setclock(&cattr, CLOCK_MONOTONIC);                                          
    CHECK(errno == 0) << (perror("pthread_condattr_setclock"), "setclock");                              

    pthread_cond_t cv;                                                                                   
    pthread_mutex_t mp;
    CHECK_EQ(pthread_cond_init(&cv, &cattr), 0);                                                                                                                   

    struct timespec timeout;    
    // 计算目标时间时,是基于CLOCK_MONOTONIC时钟计算的                                                                       
    clock_gettime(CLOCK_MONOTONIC, &timeout);                                                          
    timeout.tv_nsec += 100000000;                                                                      
    timeout.tv_sec += timeout.tv_nsec / 1000000000;                                                    
    timeout.tv_nsec %= 1000000000;                                                                     

    pthread_mutex_lock(&mp);                                                                           
    pthread_cond_timedwait(&cv, &mp, &timeout);                                                        
    pthread_mutex_unlock(&mp);   

Semaphore—使用最新的sem_clockwait接口

经过Google,发现从glibc 2.30开始提供了一个新的API:sem_clockwait:

https://stackoverflow.com/questions/40698438/sem-timedwait-with-clock-monotonic-raw-clock-monotonic

https://www.gnu.org/software/libc/manual/html_node/Waiting-with-Explicit-Clocks.html

int sem_clockwait (sem_t *sem, clockid_t clockid, const struct timespec *abstime)

新的API支持设置CLOCK类型:clockid must be either CLOCK_MONOTONIC or CLOCK_REALTIME.看了一下操作系统下的glibc版本是满足的(ldd --version).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值