redis后台线程条件变量源码解析

定义

条件变量允许一个线程就某个共享变量(或其他共享资源)的状态变化通知其他线程,并让其他线程等待(阻塞于)这一通知。

标记变量

有时候线程之间不用条件变量和锁, 用一个标记变量会看起来很简单,很吸
引人。例如,等待代码:

while (ready == 0)
    ; // spin

相关的发信号代码看起来像这样:

ready = 1;

千万不要这么做。因为:

  • 首先,多数情况下性能差(长时间的自旋浪费 CPU)。
  • 其次,容易出错。这些不正规的同步方法半数以上都是有问题的。

条件变量API

采用了条件变量( condition variable),这一问题就迎刃而解:允许一个线程休眠(等
待)直至接获另一线程的通知(收到信号)去执行某些操作(例如,出现一些“情况”后,
等待者必须立即做出响应)。线程阻塞时不占用CPU

#include 

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); 
pthread_cond_signal/pthread_cond_broadcast

函数 pthread_cond_signal()和 pthread_cond_broadcast()之间的差别在于,二者对阻塞于 pthread_cond_wait()的多个线程处理方式不同。 pthread_cond_signal()函数只保证唤醒至少一条遭到阻塞的线程,而 pthread_cond_broadcast()则会唤醒所有遭阻塞的线程。

只有当仅需唤醒一条(且无论是其中哪条)等待线程来处理共享变量的状态变化时,才应使用 pthread_cond_signal()。应用这种方式的典型情况是,所有等待线程都在执行完全相同的任务。同理,函数 pthread_cond_broadcast()所处理的情况是:处于等待状态的所有线程执行
的任务不同(即各线程关联于条件变量的判定条件不同)。

pthread_cond_wait

条件变量总是要与一个互斥量相关。将这些对象通过函数参数传递给 pthread_cond_wait(),后者执行如下操作步骤。

  • 解锁互斥量 mutex。
  • 堵塞调用线程,直至另一线程就条件变量 cond 发出信号。
  • 重新锁定 mutex。

通常情况下代码会以如下方式访问共享变量:

pthread_mutex_lock(&mtx);
while(/* Check that shared variable is not in state we want */)
    pthread_cond_wait(&cond, &mtx);
pthread_mutex_unlock(&mtx);

代码执行流程如下:

  1. 线程在准备检查共享变量状态时锁定互斥量。
  2. 检查共享变量的状态。
  3. 如果共享变量未处于预期状态,线程应在等待条件变量并进入休眠前解锁互斥量(以便其他线程能访问该共享变量)。
  4. 当线程因为条件变量的通知而被再度唤醒时, 必须对互斥量再次加锁, 因为在典型情况下,线程会立即访问共享变量。

函数 pthread_cond_wait()会自动执行最后两步中对互斥量的解锁和加锁动作。第 3 步中互斥量的释放与陷入对条件变量的等待同属于一个原子操作。换句话说,在函数pthread_cond_wait()的调用线程陷入对条件变量的等待之前,其他线程不可能获取到该互斥量,也不可能就该条件变量发出信号。

测试条件变量的判断条件( predicate)

必须由一个 while循环,而不是 if 语句,来控制对pthread_cond_wait()的调用。这是因为,当代码从 pthread_cond_wait()返回时,并不能确定判断条件的状态,所以应该立即重新检查判断条件,在条件不满足的情况下继续休眠等待。

从 pthread_cond_wait()返回时,之所以不能对判断条件的状态做任何假设,其理由如下。

  • 其他线程可能会率先醒来。也许有多个线程在等待获取与条件变量相关的互斥量。即使就互斥量发出通知的线程将判断条件置为预期状态, 其他线程依然有可能率先获取互斥量并改变相关共享变量的状态,进而改变判断条件的状态。
  • 设计时设置“宽松的”判断条件或许更为简单。有时,用条件变量来表征可能性而非确定性,在设计应用程序时会更为简单。换言之,就条件变量发送信号意味着“可能有些事情”需要接收信号的线程去响应,而不是“一定有一些事情”要做。使用这种方法,可以基于判断条件的近似情况来发送条件变量通知,接收信号的线程可以通过再次检查判断条件来确定是否真的需要做些什么。
  • 可能会发生虚假唤醒的情况。在一些实现中,即使没有任何其他线程真地就条件变量发出信号,等待此条件变量的线程仍有可能醒来。在一些多处理器系统上,为确保高效实现而采用的技术会导致此类(不常见的)虚假唤醒。

redis5.0.8源码

void *bioProcessBackgroundJobs(void *arg) {
    struct bio_job *job;
    unsigned long type = (unsigned long) arg;
    pthread_mutex_lock(&bio_mutex[type]);

    while(1) {
        /* The loop always starts with the lock hold. */
        if (listLength(bio_jobs[type]) == 0) {
            pthread_cond_wait(&bio_newjob_cond[type],&bio_mutex[type]);
            continue;
        }
        /* Pop the job from the queue. */
        ln = listFirst(bio_jobs[type]);
        job = ln->value;
        /* It is now possible to unlock the background system as we know have
         * a stand alone job structure to process.*/
        pthread_mutex_unlock(&bio_mutex[type]);

        /* Process the job accordingly to its type. */
        ......

        /* Lock again before reiterating the loop, if there are no longer
         * jobs to process we'll block again in pthread_cond_wait(). */
        pthread_mutex_lock(&bio_mutex[type]);
        listDelNode(bio_jobs[type],ln);
        bio_pending[type]--;

        /* Unblock threads blocked on bioWaitStepOfType() if any. */
        pthread_cond_broadcast(&bio_step_cond[type]);
    }
}
代码解释
  1. continue跳转到外层while,相当于将if转换为while条件。

    if (listLength(bio_jobs[type]) == 0) {
     pthread_cond_wait(&bio_newjob_cond[type],&bio_mutex[type]);
     continue;
    }
  2. 弹出bio_jobs队首任务后,即可解锁。因为bio_jobs对列是共享变量

    /* Pop the job from the queue. */
    ln = listFirst(bio_jobs[type]);
    job = ln->value;
    /* It is now possible to unlock the background system as we know have
     * a stand alone job structure to process.*/
    pthread_mutex_unlock(&bio_mutex[type]);

小结

线程提供的强大共享是有代价的。多线程应用程序必须使用互斥量和条件变量等同步原语来协调对共享变量的访问。

  • 互斥量提供了对共享变量的独占式访问。
  • 条件变量允许一个或多个线程等候通知:其他线程改变了共享变量的状态。

参考资料

  1. Linux/Unix系统编程手册
  2. redis5.0.8源码

你可能感兴趣的:(redis)