总结——线程同步

概述

最近第二遍啃APUE的11章,与第一次看天书的感觉不同。收获颇多,醍醐灌顶。
总结一下线程间的同步方式:互斥量,条件变量,读写锁,自旋锁,屏障。

互斥量

可以使用pthread的互斥接口来保护数据,确保同时只有一个线程访问数据。互斥量本质上说是一把锁,在访问共享资源前对互斥量进行加锁,访问完成后解锁互斥量。对互斥量进行加锁后,任何其他试图再次对互斥量加锁的线程都会阻塞直到当前线程释放该互斥锁。如果释放互斥量时有一个以上的线程阻塞,那么所有阻塞的线程都变为可运行状态,第一个变为可运行状态的线程可以对互斥量加锁,其他线程再次陷入阻塞。在这种情况下,每次只有一个线程可以向前执行。确保了临界区同时只能有一个线程的访问。

互斥变量使用pthread_mutext_t数据类型表示的,使用前必须进行初始化,如果是静态的互斥变量,可以直接赋初始值PTHREAD_MUTEX_INITIALIZER
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
对于动态申请(malloc)的互斥变量,必须使用pthread_mutex_init()进行初始化。使用pthread_mutext_destroy()进行内存释放。

使用pthread_mutext_lock()可以对互斥量进行加锁,如果已经被别的线程加锁,则调用线程自旋一会儿进入阻塞状态。使用pthread_mutex_unlock()可以对互斥量进行解锁。对于已经解锁的互斥量再次进行解锁的行为是未定义的。使用pthread_mutex_trylock()可以对互斥量进行加锁,但是如果该互斥量已经被锁,则调用线程不会阻塞,而是返回EBUSY

1
2
3
4
5
#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//成功返回0,出错返回错误编号

条件变量

顾名思义,当某个线程要执行某一动作时,需要满足一个条件。当另一线程对该条件做出改变时,条件变量用于通知该线程,让其继续向下执行。条件变量和互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。因为条件变量本身是由互斥量保护的,线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会觉察到这种改变,因为互斥量必须在锁定后才能计算条件。

在使用条件变量前也要进行初始化,对于静态的条件变量,可以用常量PTHREAD_COND_INITIALIZER进行初始化;动态的条件变量必须用pthread_cond_init()进行初始化,用pthread_cond_destroy()进行内存释放。

可以使用pthread_cond_wait(*cond,*mutex)等待条件变量变为真。传递给pthread_cond_wait()的互斥量对条件进行保护。调用者把锁住的互斥量传给函数,函数然后自动把调用线程放到等待条件的线程列表上,对互斥量进行解锁,这样线程就不会错过条件的任何变化。pthread_cond_wait()返回时,互斥量再次被锁住。需要注意的是:当等待线程获得锁时,需要重新计算条件,因为另一个线程可能已经在运行并改变了条件。使用pthread_cond_signal(*cond)可以唤醒至少一个等待该条件的线程,而pthread_cond_broadcast(*cond)则能唤醒所有等待该条件的线程。

1
2
3
4
5
6
7
8
#include<pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
//成功返回0,出错返回错误编号

读写锁

读写锁与互斥量类似,但是读写锁有更高的并行型。互斥量只有锁住和未锁住两种状态,而且一次只有一个线程对齐进行加锁。读写锁可以有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。当读写锁是写加锁状态时,在这个所被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是任何希望以写模式对此锁进行加锁的线程都会被阻塞,直到所有的线程释放它们的读锁为止。需要注意:在阻塞的写锁之后到达的读锁也会被阻塞,这是为了防止写锁被一直阻塞

使用pthread_rwlock_init()进行读写锁的初始化。它会为读写锁分配资源,pthread_rwlock_destroy()将释放这些资源。如果在调用pthread_rwlock_destroy()之前就释放了读写锁占有的空间,那么分配给这个锁的资源就会丢失。
调用pthread_rwlock_rdlock()可以在读模式下锁定读写锁;pthread_rwlock_wrlock()可以在写模式下锁定读写锁。不管何种模式锁住读写锁,pthread_rwlock_unlock()都可以对其进行解锁。读写锁的优势体现在当线程搜索作业的频率远高于增加或删除作业时

1
2
3
4
5
6
7
8
#include<pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *restrict rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *restrict rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//成功返回0,出错返回错误编号

自旋锁

自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等阻塞状态,当线程自旋锁变为可用时,CPU不能做其他事情。自旋锁可用于以下情况:锁被持有的时间短,而且线程并不希望在重写调度上花费太多的成本。
当自旋锁用在非抢占内核中时是非常有用的:除了提供互斥机制外,它们会阻塞中断,这样中断处理程序就不会陷入死锁状态,因为它需要获取被加锁的自旋锁。

1
2
3
4
#include<pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock,int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
//成功返回0,出错返回错误编号

pshared参数表示进程共享属性,表明自旋锁时如何获取的。如果它设为PTHREAD_PROCESS_SHARED,则自旋锁能被可以访问锁底层内存的线程所获取,即便这些线程属于不同的进程。pshared参数设为PTHREAD_PROCESS_PRIVATE,自旋锁只能被初始化该锁的进程内部的线程所访问。
使用pthread_spin_lock()可以对自旋锁进行加锁,如果已经被锁,则自旋等待获取锁。pthread_spin_unlock()可以解锁。

1
2
3
4
#include<pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
//成功返回0,出错返回错误编号

需要注意:不要调用在持有自旋锁情况下可能进入休眠状态的函数。如果调用了这些函数,会浪费CPU资源,因为其他线程获取自旋锁需要等待的时间就延长了。

屏障

屏障是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。其实pthread_join()就是一种屏障,它会等待一个对等线程的终止。屏障允许任意数量的线程等待,直到所有线程完成处理工作,而线程不需要退出,所有线程到达屏障后可以接着工作。
可以使用pthread_barrier_init()对屏障进行初始化,pthread_barrier_destroy()进行释放相应资源。使用pthread_barrier_wait()函数来表明,线程已经完成工作,准备等其他线程赶上来。

1
2
3
4
5
6
7
8
9
10
#include<pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr,
unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);
//成功返回0,出错返回错误编号

int pthread_barrier_wait(pthread_barrier_t *barrier);
//成功返回0或PTHREAD_BARRIER_SERIAL_THREAD,
//出错返回错误编号

初始化屏障时,可以使用count参数指定,在允许所有线程继续运行之前,必须到达屏障的线程数目。调用pthread_barrier_wait()的线程在屏障计数未满足条件时会进入休眠状态。如果该线程是最后一个调用pthread_barrier_wait()的线程,就满足了屏障计数,所有的线程都被唤醒。一旦达到屏障计数值,而且线程处于非阻塞状态,屏障就可以被重用。


参考资料:
《UNIX环境高级编程 3th》(APUE)