不论是多进程还是多线程程序设计中,进程或线程同步是一个非常重要的点。这里我们介绍Linux C线程同步的三种方法:

  • 互斥量(mutex)
  • 读写锁
  • 条件变量

1. 互斥量

互斥量本质上是一把锁,在访问共享资源前对共享资源加锁,在访问完以后释放互斥量上的锁。

互斥量用pthread_mutex_t数据类型来表示,在使用互斥变量之前,必须首先对它进行初始化,可以把它置为常量PTHREAD_MUTEX_INITIALIZER(只对静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态的分配互斥量(例如通过调用malloc函数),那么在释放内存前需要调用pthread_mutex_destroy。

下面是互斥量相关的函数:

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 要用默认的属性初始化互斥量,只需把attr设置为NULL
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

// 给互斥量加锁
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_mutex_lock,如果互斥量已经上锁,调用线程将阻塞直到互斥量解锁。
  • 如果线程不希望被阻塞,可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果锁处于未锁定状态,调用该函数将锁住互斥量,不会出现阻塞并返回0,否则函数调用失败,不能锁住互斥量,返回EBUSY。

2. 读写锁

读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁可以有三种状态:读模式下加锁状态;写模式下加锁状态;不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

当读写锁是写加锁状态时,在这个锁被解锁前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须阻塞到所有的线程释放读锁。虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。

下面是读写锁相关的函数:

#include <pthread.h>

// 销毁为读写锁分配的资源(在释放底层内存前调用)
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);

// 在读模式下加读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

// 在写模式下加读写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

// 释放读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

与互斥量一样,读写锁在使用之前必须初始化,在释放它底层的内存前必须摧毁:在释放读写锁占用的内存之前,需要调用pthread_rwlock_destroy做清理工作。如果pthread_rwlock_init为读写锁分配了资源,pthread_rwlock_destroy将释放这些资源。如果调用pthread_rwlock_destroy之前就释放了读写锁占用的空间,那么分配给这个锁的资源就丢了。

3. 条件变量

条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量和互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件变量本身是由互斥变量保护的。线程在改变条件状态前必须先锁定互斥量,其他线程在获得互斥量之前不会觉察到这种变化,因为必须锁定互斥量以后才能计算条件。

条件变量相关的函数如下:

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

条件变量使用之前必须初始化,pthread_cond_t数据类型代表的条件变量可以用下面两种方式进行初始化,可以把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,但是如果条件变量是动态分配的,可以使用pthread_cond_init函数进行初始化。除非需要创建一个非默认属性的条件变量,否则pthread_cond_init函数的attr参数可以设置为NULL。使用pthread_cond_wait等待条件变为真,传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传递给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作都是原子操作。这样就关闭了条件检查和线程进入睡眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。

pthread_cond_timedwait函数的工作方式与pthread_cond_wait相似,只是多了一个abstime。该参数值制定了等待的时间,如果给定时间内条件不能满足,那么函数会生成一个代表出错码的返回变量。abstime通过timespec结构指定,时间值用秒数或者分秒数来表示,分秒数的单位是纳秒。

struct timespec {
	time_t	tv_sec;
	long	tv_nsec;
}

使用这个结构时,需呀指定愿意等待多长时间,时间值是一个绝对数而不是相对数。例如,如果能等待3分钟,就需要把当前时间加上3分钟再转换到timespec结构,而不是把3分钟转换成timespec结构。我们可以使用gettimeofday获取用timeval结构表示的当前时间,然后把这个时间转换成timespec结构。要得到abstime值的绝对时间,可以使用下面的函数:

void 
make_abstime(struct timespec *tsp, long minutes)
{
	struct timeval now;
	
	/* 获取当前时间 */
	gettimeofday(&now);
	tsp->tv_sec = now.tv_sec;
	tsp->nsec = now.tv_usec * 1000;
	
	/* 加上我们要等待的时间 */
	tsp->tv_sec += minutes * 60;
}

如果时间值到了但条件还没有出现,pthread_cond_timedwait将重新获取互斥量然后返回错误ETIMEOUT。从pthread_cond_timedwait或者pthread_cond_wait调用成功返回时,线程需要重新计算条件,因为其他的线程可能已经在运行并改变了条件。

下面两个函数可以用于通知线程条件已经满足:

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal函数将唤醒等待该条件的某个线程,而pthread_cond_broadcast函数将唤醒等待该条件的所有线程。但具体实现的时候,POSIX规范为了简化实现,允许pthread_cond_signal在实现的时候可以唤醒不止一个线程。调用这两个函数,也称为向线程或者条件发送信号。必须注意一定要在改变条件状态以后再给线程发送信号。

NB:这里只是简单的介绍了一下机制以及涉及到的函数,更多细节可以在使用的时候参考man文档。