计算机系统篇之异常控制流(4):如何正确地让调用线程休眠一段时间
Author: stormQ
Created: Thursday, 20. August 2020 07:39PM
Last Modified: Friday, 18. September 2020 03:07PM
直接调用sleep
/usleep
/nanosleep
等函数在什么情况下会导致程序的实际行为不符合预期?std::this_thread::sleep_for
函数在任何情况下都是正确的吗?本文让你学会让调用线程睡眠一段时间的正确做法。
源码,proc5_main.cpp(错误用法):
#include <cstdio>
#include <unistd.h>
int main()
{
std::printf("before sleep() called...\n");
const auto left_seconds = sleep(10);
std::printf("after sleep() called..., left seconds:%d\n", left_seconds);
return 0;
}
编译:
$ g++ -o proc5_main proc5_main.cpp -g
运行:
$ ./proc5_main
before sleep() called...
after sleep() called..., left seconds:0
通常情况下,上述代码中的sleep
函数会将调用线程挂起 10 秒。但是,当进程收到一个不被忽略的信号时,sleep
函数会被中断而过早地返回,其返回值为还要休眠的秒数。也就是说,sleep
函数的实际行为不符合我们的预期结果。
为了直观地展示sleep
函数的这一异常行为,我们捕获SIGINT
信号,并且修改其默认行为。为了简单起见,SIGINT
信号的处理函数什么都不做。
修改后的 proc5_main.cpp(错误用法):
#include <cstdio>
#include <unistd.h>
#include <signal.h>
void sigint_hanlder(int)
{
// do nothing
}
int main()
{
signal(SIGINT, sigint_hanlder);
std::printf("before sleep() called...\n");
const auto left_seconds = sleep(10);
std::printf("after sleep() called..., left seconds:%d\n", left_seconds);
return 0;
}
使用与上文相同的方式进行编译后,运行:
$ ./proc5_main
在 proc5_main 退出前,在另一个 Shell 窗口中执行如下命令:
$ pkill -SIGINT -f "proc5_main"
./proc5_main 的运行结果为:
before sleep() called...
after sleep() called..., left seconds:3
可以看出,sleep
函数还要休眠的秒数为 3。也就是说,sleep
函数实际休眠的秒数为 7,而不是我们所预期的休眠 10 秒。
源码,proc6_main.cpp(错误用法):
#include <cstdio>
#include <unistd.h>
#include <signal.h>
void sigint_hanlder(int)
{
// do nothing
}
int main()
{
signal(SIGINT, sigint_hanlder);
std::printf("before usleep() called...\n");
const auto ret = usleep(10*1000*1000);
std::printf("after usleep() called, return value:%d\n", ret);
return 0;
}
编译:
$ g++ -o proc6_main proc6_main.cpp -g
运行及运行结果(在不发送信号的情况下):
$ ./proc6_main
before usleep() called...
after usleep() called, return value:0
可以看出,在不发送信号的情况下,usleep
函数的返回值为 0,表示调用成功,符合我们所预期的休眠 10 秒。
运行及运行结果(在发送信号的情况下):
$ ./proc6_main
before usleep() called...
after usleep() called, return value:-1
可以看出,在发送信号的情况下,usleep
函数的返回值为 -1,表示发生了错误而过早地返回了,不符合我们所预期的休眠 10 秒。
源码,proc7_main.cpp(错误用法):
#include <cstdio>
#include <time.h>
#include <signal.h>
void sigint_hanlder(int)
{
// do nothing
}
int main()
{
signal(SIGINT, sigint_hanlder);
std::printf("before nanosleep() called...\n");
struct timespec requested_time;
requested_time.tv_sec = 10;
requested_time.tv_nsec = 0;
struct timespec remaining_time;
remaining_time.tv_sec = 0;
remaining_time.tv_nsec = 0;
const auto ret = nanosleep(&requested_time, &remaining_time);
std::printf("after nanosleep() called, return value:%d, "
"requested_time:%ld secs, %ld nsecs, remaining_time:%ld secs, %ld nsecs\n",
ret, requested_time.tv_sec, requested_time.tv_nsec,
remaining_time.tv_sec, remaining_time.tv_nsec);
return 0;
}
编译:
$ g++ -o proc7_main proc7_main.cpp -g
运行及运行结果(在不发送信号的情况下):
$ ./proc7_main
before nanosleep() called...
after nanosleep() called, return value:0, requested_time:10 secs, 0 nsecs, remaining_time:0 secs, 0 nsecs
可以看出,在不发送信号的情况下,nanosleep
函数的返回值为 0,表示调用成功,符合我们所预期的休眠 10 秒。
运行及运行结果(在发送信号的情况下):
$ ./proc7_main
before nanosleep() called...
after nanosleep() called, return value:-1, requested_time:10 secs, 0 nsecs, remaining_time:8 secs, 644286513 nsecs
可以看出,在发送信号的情况下,nanosleep
函数的返回值为 -1,表示发生了错误而过早地返回了,还要休眠的时间为 8 秒 + 644286513 纳秒,不符合我们所预期的休眠 10 秒。
当进程收到一个信号且不被忽略时,sleep
、usleep
和nanosleep
函数会被该信号中断从而过早地返回,并且当信号处理函数执行完成后,这些函数不会继续休眠剩余的时间量。
由于sleep
函数的返回值为还要休眠的秒数。我们可以利用这一信息,在sleep
函数被信号中断时,继续休眠剩下的时间量。
在sleep
函数返回后,我们判断该函数是否被信号中断的依据为:全局变量errno
值是否等于EINTR
。如果相等,则是被信号中断的;否则,是由于其他错误而过早返回地。这一方法对nanosleep
函数同样适用。
源码,proc8_main.cpp(正确用法):
#include <cstdio>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void sigint_hanlder(int)
{
// do nothing
}
int sleep_for(int secs)
{
// If sleep interupted by a signal, try again.
while ((secs = sleep(secs)) > 0 && EINTR == errno)
{
}
return secs;
}
int main()
{
signal(SIGINT, sigint_hanlder);
std::printf("before sleep() called...\n");
const auto left_seconds = sleep_for(10);
std::printf("after sleep() called..., left seconds:%d\n", left_seconds);
return 0;
}
编译:
$ g++ -o proc8_main proc8_main.cpp -g
运行:
$ ./proc8_main
在 proc8_main 退出前,在另一个 Shell 窗口中执行如下命令:
$ pkill -SIGINT -f "proc8_main"
./proc8_main 的运行结果为:
before sleep() called...
after sleep() called..., left seconds:0
从输出结果中可以看出,在发送信号的情况下,sleep_for
函数仍会休眠 10 秒,符合我们的预期。
由于nanosleep
函数的第二个参数为还要休眠的时间量。我们可以利用这一信息,在nanosleep
函数被信号中断时,继续休眠剩下的时间量。
源码,proc9_main.cpp(正确用法):
#include <cstdio>
#include <time.h>
#include <signal.h>
#include <errno.h>
void sigint_hanlder(int)
{
// do nothing
}
void nanosleep_for(const struct timespec& requested)
{
struct timespec remaining;
remaining.tv_sec = requested.tv_sec;
remaining.tv_nsec = requested.tv_nsec;
// If nanosleep interupted by a signal, try again.
while (-1 == nanosleep(&remaining, &remaining) && EINTR == errno)
{
}
std::printf("after nanosleep() called, remaining time:%ld secs, %ld nsecs\n",
remaining.tv_sec, remaining.tv_nsec);
}
int main()
{
signal(SIGINT, sigint_hanlder);
std::printf("before nanosleep() called...\n");
struct timespec requested_time;
requested_time.tv_sec = 10;
requested_time.tv_nsec = 0;
nanosleep_for(requested_time);
std::printf("after nanosleep() called, requested time:%ld secs, %ld nsecs\n",
requested_time.tv_sec, requested_time.tv_nsec);
return 0;
}
编译:
$ g++ -o proc9_main proc9_main.cpp -g
运行:
$ ./proc9_main
在 proc9_main 退出前,在另一个 Shell 窗口中执行如下命令:
$ pkill -SIGINT -f "proc9_main"
./proc9_main 的运行结果为:
before nanosleep() called...
after nanosleep() called, remaining time:7 secs, 900349154 nsecs
after nanosleep() called, requested time:10 secs, 0 nsecs
输出结果中,出现remaining time:7 secs, 900349154 nsecs
的原因是nanosleep
函数调用成功时不会修改第二个参数的值。
通过实际运行可以发现,在发送信号的情况下,nanosleep_for
函数仍会休眠 10 秒,符合我们的预期。
源码,proc10_main.cpp(正确用法):
#include <cstdio>
#include <unistd.h>
#include <signal.h>
#include <thread>
void sigint_hanlder(int)
{
// do nothing
}
int main()
{
signal(SIGINT, sigint_hanlder);
std::printf("before std::this_thread::sleep_for() called...\n");
std::this_thread::sleep_for(std::chrono::seconds(10));
std::printf("after std::this_thread::sleep_for() called...\n");
return 0;
}
编译:
$ g++ -o proc10_main proc10_main.cpp -g
运行:
$ ./proc10_main
在 proc10_main 退出前,在另一个 Shell 窗口中执行如下命令:
$ pkill -SIGINT -f "proc10_main"
./proc10_main 的运行结果为:
before std::this_thread::sleep_for() called...
after std::this_thread::sleep_for() called...
通过实际运行可以看出,在发送信号的情况下,std::this_thread::sleep_for
函数仍会休眠 10 秒,符合我们的预期。
注意: 要想std::this_thread::sleep_for
休眠的时间量符合我们的预期,必须满足:gcc
版本 >= 6.1.0。这是因为,gcc
6.1.0 之前的版本并未处理被信号中断的情形。该 BUG 在gcc
6.1.0 及之后的版本中进行了修复,修复记录 点击这里。
下一篇:计算机系统篇之异常控制流(6):如何正确地回收子进程