引言
在程序设计中,有时需要让进程或线程暂停执行一段时间,这种需求可以通过使用 sleep 函数来实现。本文将详细介绍在 C 语言环境下可用的不同类型的 sleep 函数,包括它们的用途、参数以及注意事项,并提供一些示例代码。
目录
- 标准
sleep()
函数- 定义与原型
- 参数解释
- 返回值与错误处理
- 示例代码
- 底层原理
- 精确定时
nanosleep()
- 定义与原型
- 参数解析
- 实现精确延时
- 示例代码
- 底层原理
- 非阻塞
usleep()
- 定义与原型
- 微秒级延迟
- 使用场景与限制
- 示例代码
- 底层原理
- 线程专用
pthread_sleep()
和clock_nanosleep()
- 线程间同步
- 参数与用法
- 示例代码
- 底层原理
- 条件变量等待
pthread_cond_timedwait()
- 条件变量与定时等待
- 参数解析
- 示例代码
- 底层原理
- 总结与建议
- 参考资料
正文
1. 标准 sleep()
函数
定义与原型:
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
参数解释:
seconds
: 指定要挂起进程的秒数。
返回值与错误处理:
- 成功时返回实际睡眠的秒数。
- 失败时返回 0 并将 errno 设置为 EINTR(如果进程被信号中断)。
示例代码:
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("Sleeping for 5 seconds...\n");
if (sleep(5) == 0) {
perror("Error sleeping");
}
printf("Woke up.\n");
return 0;
}
底层原理:
sleep()
是一个系统调用,它通过发送 SIGTSTP 信号给当前进程来挂起进程。- 当进程调用
sleep()
函数时,它实际上是在调用内核中的一个系统调用处理程序。 - 系统调用处理程序会检查进程是否已经准备好进入睡眠状态,并将进程的状态标记为不可中断的睡眠状态 (D)。
- 内核会将进程添加到睡眠队列中,并开始计时。
- 当指定的时间过去后,内核会唤醒该进程,并将进程的状态从睡眠状态改为可运行状态 ®。
- 进程回到用户空间后,会继续执行。
- 如果在睡眠过程中接收到一个信号(如 SIGALRM 或 SIGTERM),则进程会被立即唤醒,
sleep()
函数会返回 0 并将 errno 设置为 EINTR。 sleep()
函数的精度相对较低,因为它只能以整秒为单位进行延时,不适合需要更高精度的应用场景。- 由于
sleep()
依赖于信号机制,因此在高负载情况下可能会出现延迟唤醒的情况。 - 使用
sleep()
时需要注意避免被信号中断的问题,尤其是当需要精确控制延时时。
- 当进程调用
2. 精确定时 nanosleep()
定义与原型:
#include <time.h>
int nanosleep(const struct timespec *req, struct timespec *rem);
参数解析:
req
: 请求的持续时间。rem
: 剩余的时间(如果请求的睡眠时间超过实际的睡眠时间)。
实现精确延时:
可以精确到纳秒级别。
示例代码:
#include <stdio.h>
#include <time.h>
int main(void) {
struct timespec ts, rem;
ts.tv_sec = 1; // 秒
ts.tv_nsec = 500000000; // 纳秒
printf("Sleeping for 1.5 seconds...\n");
if (nanosleep(&ts, &rem) != 0) {
perror("Error in nanosleep");
} else {
printf("Woke up.\n");
}
return 0;
}
底层原理:
nanosleep()
使用的是基于时钟的定时机制,而不是简单的信号机制。- 当调用
nanosleep()
时,内核会检查req
参数中的时间值,并开始计时。 - 内核使用一个高精度的时钟来计时,并在达到指定时间时唤醒进程。
- 如果进程在睡眠期间被信号中断,
nanosleep()
会将剩余的睡眠时间保存在rem
结构体中。 nanosleep()
支持中断处理,当进程被信号中断时,剩余的睡眠时间会保存在rem
结构体中,可以在后续调用中使用。- 由于
nanosleep()
可以精确到纳秒,因此它非常适合需要高精度延时的应用场景。 nanosleep()
函数的实现依赖于内核提供的高精度时钟源,这使得它可以提供比sleep()
更高的精度。- 在现代操作系统中,
nanosleep()
使用的是实时时钟,这意味着即使系统处于睡眠状态,计时也不会受到影响。 - 使用
nanosleep()
时需要注意,尽管它可以提供较高的精度,但在高负载情况下也可能受到一定的影响。
- 当调用
3. 非阻塞 usleep()
定义与原型:
#include <unistd.h>
int usleep(useconds_t usec);
微秒级延迟:
适用于需要更精细控制的情况。
使用场景与限制:
- 微秒级别的精度。
- 注意可能存在的不精确性。
示例代码:
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("Sleeping for 500 milliseconds...\n");
if (usleep(500000) != 0) {
perror("Error in usleep");
} else {
printf("Woke up.\n");
}
return 0;
}
底层原理:
usleep()
通常不是通过直接的系统调用来实现的,而是通过其他延时机制,如select()
或者poll()
来模拟实现。usleep()
的实现依赖于底层操作系统的实现细节。- 在某些实现中,
usleep()
可能会使用select()
函数来实现延时功能。 select()
函数允许进程在一个或多个文件描述符上等待事件的发生,这里可以设置一个空的文件描述符列表,并设置一个延时时间。- 当延时时间到达后,
select()
会返回,从而结束usleep()
的调用。 - 由于
usleep()
不是标准的系统调用,所以它的实现可能会因操作系统而异。 usleep()
的主要问题是它可能受到系统负载的影响,导致实际的延时时间比预期的长。- 在某些情况下,
usleep()
可能会使用poll()
函数来实现,这取决于操作系统的具体实现。 - 使用
usleep()
时需要注意,虽然它可以提供微秒级别的精度,但实际延时时间可能会受到系统负载和其他因素的影响。
4. 线程专用 pthread_sleep()
和 clock_nanosleep()
线程间同步:
专门用于线程的延时函数。
参数与用法:
clock_nanosleep()
更加灵活且具有更好的性能。
示例代码:
#include <stdio.h>
#include <pthread.h>
#include <time.h>
void* thread_func(void *arg) {
struct timespec ts;
ts.tv_sec = 2; // 秒
ts.tv_nsec = 0; // 纳秒
printf("Thread: Sleeping for 2 seconds...\n");
clock_nanosleep(CLOCK_REALTIME, 0, &ts, NULL);
printf("Thread: Woke up.\n");
pthread_exit(NULL);
}
int main(void) {
pthread_t thread;
if (pthread_create(&thread, NULL, thread_func, NULL) != 0) {
perror("Error creating thread");
return 1;
}
if (pthread_join(thread, NULL) != 0) {
perror("Error joining thread");
return 1;
}
return 0;
}
底层原理:
pthread_sleep()
和clock_nanosleep()
都是为了支持多线程环境中的延时操作而设计的。clock_nanosleep()
使用的是线程的调度器,而不是整个进程的调度器。- 当一个线程进入
clock_nanosleep()
后,其他线程仍然可以继续运行。 clock_nanosleep()
提供了更高的精度,并且不会影响到其他线程的执行。- 内核维护了一个高精度的时钟,当线程调用
clock_nanosleep()
时,内核会记录下线程需要睡眠的时间,并在达到这个时间时唤醒线程。 - 由于
clock_nanosleep()
是针对单个线程的操作,因此它不会影响到其他线程的调度。 clock_nanosleep()
使用的时钟源通常是实时时钟,这意味着即使系统处于睡眠状态,计时也不会受到影响。- 使用
clock_nanosleep()
时需要注意,尽管它可以提供较高的精度,但在高负载情况下也可能受到一定的影响。
5. 条件变量等待 pthread_cond_timedwait()
条件变量与定时等待:
结合条件变量进行定时等待,常用于多线程同步。
示例代码:
#include <stdio.h>
#include <pthread.h>
#include <time.h>
int main(void) {
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;
void *thread_func(void *arg) {
struct timespec ts;
ts.tv_sec = time(NULL) + 5; // 当前时间加上5秒
ts.tv_nsec = 0;
pthread_mutex_lock(&mutex);
while (!ready) {
printf("Thread: Waiting until %ld seconds...\n", ts.tv_sec);
if (pthread_cond_timedwait(&cond, &mutex, &ts) != 0) {
perror("Error in pthread_cond_timedwait");
break;
}
}
printf("Thread: Ready flag is set.\n");
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
pthread_t thread;
if (pthread_create(&thread, NULL, thread_func, NULL) != 0) {
perror("Error creating thread");
return 1;
}
sleep(3); // 等待3秒后设置标志
pthread_mutex_lock(&mutex);
ready = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
if (pthread_join(thread, NULL) != 0) {
perror("Error joining thread");
return 1;
}
return 0;
}
底层原理:
pthread_cond_timedwait()
用于在条件变量上等待,直到满足某个条件或者超时为止。- 当线程调用
pthread_cond_timedwait()
时,它会释放互斥锁并进入等待状态。 - 如果条件满足或者到达了指定的时间,线程就会被唤醒并重新获取互斥锁。
- 使用条件变量可以有效地同步多个线程的执行顺序。
- 当一个线程调用
pthread_cond_timedwait()
时,内核会检查当前时间与请求的时间,如果当前时间未达到请求的时间,线程会被加入到条件变量的等待队列中。 - 内核会定期检查条件变量队列中的线程是否可以被唤醒。
- 当条件满足时,调用
pthread_cond_signal()
或pthread_cond_broadcast()
会唤醒一个或所有等待的线程。 - 被唤醒的线程会重新获得互斥锁并继续执行。
- 使用
pthread_cond_timedwait()
时需要注意,尽管它可以提供精确的定时等待,但在高负载情况下也可能受到一定的影响。
- 当线程调用
6. 总结与建议
- 选择合适的函数: 根据精度要求和使用场景选择合适的延时函数。
- 注意中断处理: 考虑到信号中断的可能性。
- 测试与验证: 在实际应用中进行充分的测试以确保正确性。
- 考虑系统负载: 在高负载情况下,即使是精确的延时函数也可能出现偏差。