计算机系统篇之异常控制流(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函数在任何情况下都是正确的吗?本文让你学会让调用线程睡眠一段时间的正确做法。

错误做法


错误做法 1:使用 sleep 让调用线程休眠一段时间(单位:秒)

源码,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 秒。


错误做法 2:使用 usleep 让调用线程休眠一段时间(单位:微秒)

源码,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 秒。


错误做法 3:使用 nanosleep 让调用线程休眠一段时间(单位:纳秒)

源码,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 秒。


原因分析

当进程收到一个信号且不被忽略时,sleepusleepnanosleep函数会被该信号中断从而过早地返回,并且当信号处理函数执行完成后,这些函数不会继续休眠剩余的时间量。


正确做法


正确做法 1:使用 sleep 让调用线程休眠一段时间(单位:秒)

由于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 秒,符合我们的预期。


正确做法 2:使用 nanosleep 让调用线程休眠一段时间(单位:微秒)

由于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() calledremaining time:7 secs, 900349154 nsecs
after nanosleep() calledrequested time:10 secs, 0 nsecs

输出结果中,出现remaining time:7 secs, 900349154 nsecs的原因是nanosleep函数调用成功时不会修改第二个参数的值。

通过实际运行可以发现,在发送信号的情况下,nanosleep_for函数仍会休眠 10 秒,符合我们的预期。


正确做法 3(推荐做法):使用 std::this_thread::sleep_for 让调用线程休眠一段时间

源码,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。这是因为,gcc6.1.0 之前的版本并未处理被信号中断的情形。该 BUG 在gcc6.1.0 及之后的版本中进行了修复,修复记录 点击这里


下一篇:计算机系统篇之异常控制流(6):如何正确地回收子进程

上一篇:计算机系统篇之链接(18):如何避免未使用的且直接依赖的共享库

首页