sem_timedwait问题概述

系统时间跳变会导致一些系统时间依赖函数的异常工作,一个例子是sem_timedwait。当处于sem_timedwait阻塞等待时,如果此时系统时间跳变到起始时间之前:

1
sudo date -s "2025-06-30 9:40:00" //修改系统时间

那么sem_timedwait会处于持续的阻塞状态,sem_timedwait不会返回,也不会产生任何打印效果,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <time.h>
#include <semaphore.h>
#include <thread>

using namespace std;

struct timespec ts{};

int main(){
sem_t sem_;
sem_init(&sem_,0,0);
while(true){
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 20;
std::this_thread::sleep_for(std::chrono::seconds(10)); //在此延时内向前改变时间
if(sem_timedwait(&sem_, &ts)){ //没有获取到信号量
cout << "sem_timewait exit!" << endl;
break;
}else{ //获取到信号量
cout << "got semphore" <<endl;
break;
}
cout << "on Loop!" <<endl;
}

cout << "done" <<endl;
return 0;
}

问题解决:sem_trywait + 超时等待实现sem_timedwait

此处的解决方法是通过sem_trywait去检测信号量,通过延时时间作超时等待,可以和sem_timedwait等效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int mySemWait(sem_t* sem, int64_t waitTime_ms){
const int64_t MaxWaitTime = 10; //每次最大轮询时间为10ms
int64_t wait_pertime = 1; //单次等待时间,从1ms逐次递增

std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now();
int64_t duration = 0;

do{
if(sem_trywait(sem) == 0){
return 0;
}
if(errno != EAGAIN){
return -1;
}
int64_t leftTime = waitTime_ms - duration;
std::this_thread::sleep_for(std::chrono::milliseconds(std::min(leftTime, std::min(wait_pertime, MaxWaitTime))));
wait_pertime *= 2;
std::chrono::steady_clock::time_point endTime = std::chrono::steady_clock::now();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
}while(duration < waitTime_ms);

return -1;
}

std::condition_variable wait_for时跳问题

如果使用系统时钟,一些版本的std::condition_variable向前时跳后使用wait_for/wait_util都会造成线程长久阻塞,如Linux下这样的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <condition_variable>
#include <chrono>
#include <thread>

using namespace std;
using std::endl;
using std::cout;

std::condition_variable cv; //条件变量

std::mutex mtx;

int main() {
auto begin = std::chrono::steady_clock::now();

auto beginTime = std::chrono::system_clock::now();

// 转换为 time_t
std::time_t beginTimet = std::chrono::system_clock::to_time_t(beginTime);

std::cout << "BeginPoint: " << std::ctime(&beginTimet);

std::thread t([](){
std::unique_lock<std::mutex> lock(mtx);
cout << "son thread..." << endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
//cv.notify_one(); //不唤醒
});
t.detach();

std::unique_lock<std::mutex> lock(mtx);
cout << "Please change the time within 10s ! " << endl;
cv.wait_for(lock, std::chrono::seconds(10)); //正常等待10s超时即继续,设置向前时跳后会长久阻塞

cout << "done" << endl;

auto end = std::chrono::steady_clock::now();
cout << std::chrono::duration_cast<std::chrono::seconds>(end - begin).count() << endl;

auto endTime = std::chrono::system_clock::now();
std::time_t endTimet = std::chrono::system_clock::to_time_t(endTime);
std::cout << "EndPoint: " << std::ctime(&endTimet);

return 0;
}

解决这样的问题,最保守的方法是引入boost库条件变量,因为发现早期gcc(gcc 8/9)等steady clock极其不完善,查看其wait_for实现,发现其通过宏去兼容系统时钟和稳定时钟,类似:

1
2
3
4
5
6
7
8
9
10
11
template<typename _Lock, typename _Rep, typename _Period>
cv_status
wait_for(_Lock& __lock, const chrono::duration<_Rep, _Period>& __rtime)
{ return wait_until(__lock, __clock_t::now() + __rtime); }

template<typename _Lock, typename _Rep,
typename _Period, typename _Predicate>
bool
wait_for(_Lock& __lock,
const chrono::duration<_Rep, _Period>& __rtime, _Predicate __p)
{ return wait_until(__lock, __clock_t::now() + __rtime, std::move(__p)); }
其中__clock_t::now()存在两种实现:
1
2
3
4
5
#ifdef _GLIBCXX_USE_PTHREAD_COND_CLOCKWAIT
using __clock_t = steady_clock;
#else
using __clock_t = system_clock;
#endif

但有可能尽管定义了相关的宏、glibc库等在2.31版本以上,仍然没有成功避免阻塞问题,特别是存在多个gcc环境时,需要使用上述demo二次验证。

boost库是最干净利落的,其果断抛弃了系统时钟的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <condition_variable>
#include <chrono>
#include <thread>
#include <boost/thread.hpp>
#include <boost/chrono.hpp>

using namespace std;
using std::endl;
using std::cout;

boost::condition_variable cv; //条件变量
boost::mutex mtx;

int main() {
auto begin = std::chrono::steady_clock::now();

auto beginTime = std::chrono::system_clock::now();

// 转换为 time_t
std::time_t beginTimet = std::chrono::system_clock::to_time_t(beginTime);

std::cout << "111BeginPoint: " << std::ctime(&beginTimet);

std::thread t([](){
boost::unique_lock<boost::mutex> lock(mtx);
cout << "222son thread..." << endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
//cv.notify_one(); //不唤醒
});
t.detach();

boost::unique_lock<boost::mutex> lock(mtx);
cout << "111111Please change the time within 10s ! " << endl;
cv.wait_for(lock, boost::chrono::seconds(10));
//cv.wait_until(lock, std::chrono::steady_clock::now() + std::chrono::seconds(10));
cout << "done" << endl;

auto end = std::chrono::steady_clock::now();
cout << std::chrono::duration_cast<std::chrono::seconds>(end - begin).count() << endl;

auto endTime = std::chrono::system_clock::now();
std::time_t endTimet = std::chrono::system_clock::to_time_t(endTime);
std::cout << "111EndPoint: " << std::ctime(&endTimet);

return 0;
}

参考链接:

修改系统时间,导致sem_timedwait 一直阻塞的问题解决和分析