MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言条件变量的超时等待

2022-07-244.8k 阅读

Linux C 语言条件变量的超时等待

条件变量概述

在 Linux 多线程编程中,条件变量(Condition Variable)是一种同步机制,它允许线程阻塞并等待某个特定条件变为真。条件变量通常与互斥锁(Mutex)一起使用,以实现线程间的复杂同步。当一个线程需要等待某个条件满足才能继续执行时,它可以使用条件变量进入等待状态。同时,其他线程在条件满足时可以通知等待的线程,使其从等待状态中唤醒并继续执行。

条件变量的基本操作

  1. 初始化条件变量 在 Linux 中,可以使用 pthread_cond_init 函数来初始化条件变量。该函数有两个参数,第一个参数是指向要初始化的条件变量的指针,第二个参数通常为 NULL,表示使用默认属性。
#include <pthread.h>

pthread_cond_t cond;
pthread_mutex_t mutex;

int main() {
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);
    // 其他代码
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}
  1. 等待条件变量 线程通过调用 pthread_cond_wait 函数来等待条件变量。该函数会自动释放与之关联的互斥锁,使线程进入等待状态。当条件变量被其他线程唤醒时,pthread_cond_wait 函数会重新获取互斥锁,然后线程继续执行。
pthread_mutex_lock(&mutex);
while (/* 条件不满足 */) {
    pthread_cond_wait(&cond, &mutex);
}
// 条件满足后的操作
pthread_mutex_unlock(&mutex);
  1. 唤醒条件变量 有两种方式可以唤醒等待条件变量的线程,分别是 pthread_cond_signalpthread_cond_broadcastpthread_cond_signal 函数会唤醒一个等待条件变量的线程,而 pthread_cond_broadcast 函数会唤醒所有等待条件变量的线程。
pthread_mutex_lock(&mutex);
// 改变条件使其满足
pthread_cond_signal(&cond);
// 或者 pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);

超时等待的需求

在实际应用中,有时我们不希望线程无限期地等待条件变量。例如,在网络通信中,等待接收数据的线程如果长时间没有接收到数据,可能需要超时处理,以避免线程一直阻塞。这时就需要用到条件变量的超时等待功能。

超时等待函数

在 Linux 中,pthread_cond_timedwait 函数用于实现条件变量的超时等待。该函数的原型如下:

int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                           pthread_mutex_t *restrict mutex,
                           const struct timespec *restrict abstime);
  • cond:指向要等待的条件变量的指针。
  • mutex:指向与条件变量关联的互斥锁的指针。
  • abstime:指向一个 struct timespec 结构体的指针,该结构体指定了等待的超时时间。struct timespec 结构体定义如下:
struct timespec {
    time_t tv_sec;  /* 秒 */
    long tv_nsec;   /* 纳秒 */
};

abstime 是一个绝对时间,即从 1970 年 1 月 1 日 00:00:00 UTC 开始到超时时刻的时间。例如,如果当前时间是 10:00:00,要设置 10 秒后的超时时间,那么 abstime 应该设置为 10:00:10 的时间值。

计算超时时间

为了设置正确的超时时间,我们通常需要获取当前时间并在此基础上加上期望的超时时间间隔。可以使用 clock_gettime 函数来获取当前时间,该函数的原型如下:

int clock_gettime(clockid_t clk_id, struct timespec *tp);
  • clk_id:指定要获取时间的时钟类型,通常使用 CLOCK_REALTIME,表示系统实时时钟。
  • tp:指向一个 struct timespec 结构体的指针,用于存储获取到的当前时间。

下面是一个计算超时时间的示例代码:

#include <time.h>
#include <stdio.h>

int main() {
    struct timespec now, timeout;
    clock_gettime(CLOCK_REALTIME, &now);
    // 设置 5 秒的超时时间
    timeout.tv_sec = now.tv_sec + 5;
    timeout.tv_nsec = now.tv_nsec;
    printf("Current time: %ld seconds, %ld nanoseconds\n", now.tv_sec, now.tv_nsec);
    printf("Timeout time: %ld seconds, %ld nanoseconds\n", timeout.tv_sec, timeout.tv_nsec);
    return 0;
}

超时等待的代码示例

下面是一个完整的示例代码,展示了如何在 Linux C 语言中使用条件变量的超时等待功能。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int ready = 0;

void *thread_function(void *arg) {
    sleep(3);
    pthread_mutex_lock(&mutex);
    ready = 1;
    printf("Thread: Signaling condition variable\n");
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t tid;
    struct timespec timeout;
    clock_gettime(CLOCK_REALTIME, &timeout);
    // 设置 2 秒的超时时间
    timeout.tv_sec += 2;

    pthread_create(&tid, NULL, thread_function, NULL);

    pthread_mutex_lock(&mutex);
    int result = pthread_cond_timedwait(&cond, &mutex, &timeout);
    if (result == 0) {
        printf("Main: Condition variable signaled\n");
    } else if (result == ETIMEDOUT) {
        printf("Main: Timeout occurred\n");
    } else {
        printf("Main: Unexpected error %d\n", result);
    }
    pthread_mutex_unlock(&mutex);

    pthread_join(tid, NULL);

    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}

在这个示例中,主线程创建了一个新线程,并设置了 2 秒的超时等待时间。新线程在 3 秒后唤醒条件变量,但主线程在 2 秒后就会超时,因此会输出 “Main: Timeout occurred”。

超时等待的应用场景

  1. 网络通信 在网络编程中,等待接收数据的线程可以设置超时时间。如果在超时时间内没有接收到数据,线程可以进行相应的错误处理,例如关闭连接或重新尝试发送请求。
  2. 资源获取 当多个线程竞争获取有限的资源时,如果某个线程等待资源的时间过长,可能需要超时处理,以避免线程一直阻塞。例如,数据库连接池中的连接获取操作,如果在一定时间内无法获取到可用连接,线程可以返回错误信息。
  3. 任务调度 在任务调度系统中,线程可能需要等待任务的执行条件满足。如果等待时间过长,可能需要重新调度任务或执行其他替代任务。

注意事项

  1. 绝对时间的设置 由于 pthread_cond_timedwait 使用的是绝对时间,在计算超时时间时要确保准确。特别是在多线程环境下,时间的获取和计算需要保证线程安全。
  2. 互斥锁的使用 条件变量的等待和唤醒操作都需要在持有互斥锁的情况下进行。在调用 pthread_cond_timedwait 之前,必须先获取互斥锁,并且在函数返回时,互斥锁会被重新获取。因此,在等待期间,其他线程对共享资源的访问需要通过互斥锁进行保护。
  3. 错误处理 pthread_cond_timedwait 函数可能会返回多种错误代码,除了 ETIMEDOUT 表示超时外,还可能有其他错误,如 EINVAL(无效的参数)等。在实际应用中,需要对这些错误进行适当的处理。

条件变量超时等待与其他同步机制的结合

  1. 与信号量的结合 信号量(Semaphore)用于控制对共享资源的访问数量。在一些场景下,可以将条件变量的超时等待与信号量结合使用。例如,当一个线程需要获取多个资源时,可以先使用信号量获取资源,然后使用条件变量等待资源准备好。如果在超时时间内资源没有准备好,可以释放已经获取的信号量,以避免资源的浪费。
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
sem_t sem1, sem2;
int ready1 = 0;
int ready2 = 0;

void *thread_function(void *arg) {
    sleep(3);
    pthread_mutex_lock(&mutex);
    ready1 = 1;
    ready2 = 1;
    printf("Thread: Signaling condition variable\n");
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t tid;
    struct timespec timeout;
    clock_gettime(CLOCK_REALTIME, &timeout);
    timeout.tv_sec += 2;

    sem_init(&sem1, 0, 1);
    sem_init(&sem2, 0, 1);

    pthread_create(&tid, NULL, thread_function, NULL);

    // 获取信号量
    sem_wait(&sem1);
    sem_wait(&sem2);

    pthread_mutex_lock(&mutex);
    int result = pthread_cond_timedwait(&cond, &mutex, &timeout);
    if (result == 0) {
        printf("Main: Condition variable signaled\n");
    } else if (result == ETIMEDOUT) {
        printf("Main: Timeout occurred, releasing semaphores\n");
        sem_post(&sem1);
        sem_post(&sem2);
    } else {
        printf("Main: Unexpected error %d\n", result);
    }
    pthread_mutex_unlock(&mutex);

    pthread_join(tid, NULL);

    sem_destroy(&sem1);
    sem_destroy(&sem2);
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}
  1. 与读写锁的结合 读写锁(Read - Write Lock)用于控制对共享资源的读写访问。在读写锁的场景下,读线程可以同时访问共享资源,而写线程需要独占访问。可以使用条件变量的超时等待来控制写线程等待共享资源变为可写状态的时间。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int data = 0;
int ready_to_write = 0;

void *read_thread_function(void *arg) {
    pthread_rwlock_rdlock(&rwlock);
    printf("Read thread: Reading data %d\n", data);
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

void *write_thread_function(void *arg) {
    struct timespec timeout;
    clock_gettime(CLOCK_REALTIME, &timeout);
    timeout.tv_sec += 2;

    pthread_mutex_lock(&mutex);
    int result = pthread_cond_timedwait(&cond, &mutex, &timeout);
    if (result == 0) {
        pthread_rwlock_wrlock(&rwlock);
        data++;
        printf("Write thread: Writing data %d\n", data);
        pthread_rwlock_unlock(&rwlock);
    } else if (result == ETIMEDOUT) {
        printf("Write thread: Timeout occurred\n");
    } else {
        printf("Write thread: Unexpected error %d\n", result);
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t read_tid, write_tid;

    pthread_create(&read_tid, NULL, read_thread_function, NULL);
    pthread_create(&write_tid, NULL, write_thread_function, NULL);

    sleep(1);
    pthread_mutex_lock(&mutex);
    ready_to_write = 1;
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);

    pthread_join(read_tid, NULL);
    pthread_join(write_tid, NULL);

    pthread_rwlock_destroy(&rwlock);
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}

性能优化与考量

  1. 减少等待时间 虽然超时等待提供了一种控制线程等待时间的机制,但在设计系统时,应尽量减少不必要的等待。例如,可以通过优化算法或调整资源分配策略,使条件更快地满足,从而避免线程长时间等待。
  2. 时间精度与开销 在设置超时时间时,要考虑时间精度和系统开销之间的平衡。如果设置的超时时间精度过高(如纳秒级),可能会增加系统的开销,因为获取高精度时间需要更多的系统资源。在大多数情况下,秒级或毫秒级的精度已经足够满足实际需求。
  3. 多线程竞争与调度 在多线程环境下,多个线程同时等待条件变量的超时可能会导致竞争和调度问题。例如,当一个线程超时后,可能需要重新调度其他线程,这可能会带来额外的开销。因此,在设计多线程系统时,需要合理规划线程的数量和等待逻辑,以减少竞争和调度开销。

总结

Linux C 语言中的条件变量超时等待是一种强大的同步机制,它为多线程编程提供了更灵活的控制。通过合理使用条件变量的超时等待功能,并结合其他同步机制,我们可以开发出高效、稳定的多线程应用程序。在实际应用中,需要注意超时时间的设置、互斥锁的使用以及错误处理等方面,以确保程序的正确性和性能。同时,通过优化等待逻辑和资源分配,可以进一步提升系统的整体性能。