多线程中的轮询与条件变量

一. 轮询

试想这样一种情形,  一个主线程中创建两个(多个)工作线程, 主线程需要等待一个或者多个工作线程执行结束.

比较直接的做法就是主线程中用一个循环, 每次都锁住和工作线程相同的一个互斥锁并检查是否有工作线程已经结束.

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t data_mutex;
int flag;

void *work_thread(void *arg)
{
    puts("working");
    usleep(2000); // do something

    pthread_mutex_lock(&data_mutex);

    puts("finished");
    ++flag; // update status

    pthread_mutex_unlock(&data_mutex);

    return NULL;
}

void main_thread()
{
    pthread_mutex_init(&data_mutex, NULL);

    flag = 0;

    pthread_t tid[2];
    pthread_create(tid + 0, NULL, work_thread, NULL);
    pthread_create(tid + 1, NULL, work_thread, NULL);

    while(1)
    {
        puts("polling");

        pthread_mutex_lock(&data_mutex);

        if(flag == 2)
        {
            puts("both working threads are finished");
            pthread_mutex_unlock(&data_mutex);
            break;
        }

        pthread_mutex_unlock(&data_mutex);

        usleep(50);
    }
}

这种方法就是传说中的轮询, 非常的simple, quick, dirty. 它的缺点是非常浪费cpu资源, 而且还会和其他线程竞争互斥锁(或其他锁), 相当的低效

(ps 这里当然可以把睡眠的时间粒度调整更大, 以达到节省资源的目的, 但这样时间精度又不够高)

要避免这种低效就要用到条件变量

二. 条件变量

条件变量的数据类型为pthread_cond_t, 与之相关的3个最简单的函数为

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_init函数用于初始化条件变量
pthread_cond_wait用于等待条件变为真
pthread_cond_signal则是发送一个唤醒信号, 也就是使条件变为真.

后面两个函数配合使用, 便是比前面的polling更高级的方案

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t data_mutex;
pthread_cond_t data_cond;
int flag;

void *work_thread(void *arg)
{
    puts("working");
    usleep(2000); // do something

    pthread_mutex_lock(&data_mutex);

    puts("finished");
    ++flag; // update status

    pthread_cond_signal(&data_cond);
    pthread_mutex_unlock(&data_mutex);

    return NULL;
}

void main_thread()
{
    pthread_mutex_init(&data_mutex, NULL);
    pthread_cond_init(&data_cond, NULL);

    flag = 0;

    pthread_t tid[2];
    pthread_create(tid + 0, NULL, work_thread, NULL);
    pthread_create(tid + 1, NULL, work_thread, NULL);

    pthread_mutex_lock(&data_mutex);

    while(flag != 2)
    {
        pthread_cond_wait(&data_cond, &data_mutex);
        puts("recieved a cond signal");
    }

    pthread_mutex_unlock(&data_mutex);
}

这里pthread_cond_wait 和前面polling中sleep会有相同的效果, 那就是将主线程投入休眠状态

而不同的是sleep相当于定时器, 睡眠的时间是定值, 而pthread_cond_wait会等到条件为真, 也就是其他线程调用了pthread_cond_signal之后才会被唤醒

pthread_cond_wait 在被调用时将互斥量解锁, 接着将当前线程投入休眠, 条件为真时唤醒当前线程并锁住互斥量

比较难理解的地方可能就是为什么条件变量一定需要一个互斥锁来保护呢?

对于这个问题, 其实可以看看前面代码中主线程的循环, 这个算是条件变量的常规使用模型
当线程被唤醒时, 还需要做一系列的判断(比如while中的 flag != 2)和数据读写之类操作, 而这些数据正好是会被工作线程所修改的, 为了保持同步就需要这么一个锁.

还有个问题是关于pthread_cond_signal函数和pthread_mutex_unlock的顺序

为了方便描述, 假设M线程为等待条件为真的主线程, W1和W2为工作线程

1. pthread_mutex_unlock先于pthread_cond_signal的情形:

如果W1线程先执行完, 于是对互斥量解锁再发送唤醒信号, 这时M线程不一定会被唤醒, 即使条件已经为真.

因为在发送条件信号之前, W1已经释放了互斥量, 这个锁可能立即会被W2获取, 这时pthread_cond_wait函数是无法占有互斥锁(因为要从 cond_wait 返回必须同时满足条件信号到来和锁已经释放), 便不会唤醒正在等待的M线程. 而如果W2会执行很久之后才释放互斥量, 那M线程就只能一直等待了

2. pthread_cond_signal先于pthread_mutex_unlock的情形:

W1先执行结束, 发送唤醒信号再解锁, pthread_cond_signal执行之后, M线程也不会立即被唤醒, 而是直到pthread_mutex_unlock将锁释放

即使这时有W2也来竞争互斥锁, 按照一般的调度实现, pthread_cond_wait 会比pthread_mutex_lock有优先获得mutex的能力. 即使这两个函数对互斥量是公平竞争的关系, 对M线程来说获得锁的机会也比前面一种情形更大