除了计算机体系结构的因素以外,程序使用变量的方式也会引起竞争,也会导致不一致的情况发生。例如,可能会对某个变量加1,然后基于这个数值做出某种决定。增量操作这一步和做出决定这一步两者的组合并非原子操作,因而给不一致情况提供了可能。
1.互斥量
可以通过使用pthread的互斥接口保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。在设计时需要规定所有的线程必须遵守相同的数据访问规则,只有这样,互斥机制才能正常工作。
- #include <pthread.h>
- int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值:若成功则返回0,否则返回错误编号
互斥变量用pthread_mutex_t数据类型来表示,在使用互斥变量以前,必须首先对它进行初始化,可以把它置为常量PTHREAD_MUTEX_INITIALIZER(只对静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态地分配互斥量(例如通过调用malloc函数),那么在释放内存前需要调用pthread_mutex_destroy。要用默认的属性初始化互斥量,只需把attr设置为NULL。
- #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_mutex_lock,如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,需要调用pthread_mutex_unlock。如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞并返回0,否则pthread_mutex_trylock就会失败,不能锁住互斥量,而返回EBUSY。
2.避免死锁
如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态。可以通过小心地控制互斥量加锁的顺序来避免死锁的发生。只有在一个线程试图以另一个线程相反的顺序锁住互斥量时,才可能出现死锁。也可以使用pthread_mutex_trylock接口避免死锁。如果已经占有某些锁而且pthread_mutex_trylock接口返回成功,那么就可以前进;但是,如果不能获取锁,可以先释放已占有的锁,做好清理工作,然后过一段时间重新尝试。
多线程的软件设计经常要考虑以下的折中处理方案。如果锁的粒度太粗,就会出现很多线程阻塞等待相同的锁,源自并发性的改善微乎其微。如果锁的粒度太细,那么过多的锁开销会使系统性能受到影响,而且代码变得相当复杂。作为一个程序员,需要在满足锁需求的情况下,在代码发杂性和优化性之间找好平衡点。
3.读写锁
读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁可以有三种状态:读模式下的加锁状态,写模式下的加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须阻塞知道所有的线程释放读锁。
读写锁非常适合于对数据结构读的次数远大于写的情况。读写锁也叫共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的,当它以写模式锁住时,它是以独占模式锁住的。
- #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);
返回值:若成功返回0,否则返回错误编号
与互斥量一样,读写锁在使用之前必须初始化,在释放它们底层的内存前必须销毁。如果希望读写锁有默认的属性,可以传一个空指针给attr。
- #include <pthread.h>
- int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
- int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
- int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
返回值:若成功返回0,否则返回错误编号
要在读模式下锁定读写锁,需要调用pthread_rwlock_rdlock;要在写模式下锁定读写锁,需要调用pthread_rwlock_wrlock。不管以何种方式锁住读写锁,都可以调用pthread_rwlock_unlock进行解锁。
- #include <pthread.h>
- int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
- int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
返回值:若成功返回0,否则返回错误EBUSY
4.条件变量
条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量。
- #include <pthread.h>
- int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
- int pthread_cond_destroy(pthread_cond_t *cond);
返回值:若成功返回0,否则返回错误编号
除非需要创建一个非默认属性的条件变量,否则pthread_cond_init函数的attr参数可以设置为NULL。
- #include <pthread.h>
- int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
- int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex,const struct timespec *restrict tmeout);
返回值:若成功返回0,否则返回错误编号
使用pthread_cond_wait等待条件变为真,如果在给定的时间内条件不能满足,那么会生成一个代表出错码的返回变量。传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。pthread_cond_wait返回时,互斥量再次被锁住。
pthread_cond_timedwait函数的工作方式与pthread_cond_wait函数相似,只是多了一个timeout。timeout值指定了等待的时间,它是通过timespec结构指定。时间值是一个绝对数而不是相对数。如果时间值到了但是条件还是没有出现,pthread_cond_timewait将重新获取互斥量然后返回错误ETIMEDOUT。
- #include <pthread.h>
- int pthread_cond_signal(pthread_cond_t *cond);
- int pthread_cond_broadcast(pthread_cond_t *cond);
返回值:若成功返回0,否则返回错误编号
这两个函数可以用于通知线程条件已经满足。int pthread_cond_signal函数将唤醒等待该条件的某个线程,而int pthread_cond_broadcast函数将唤醒等待该条件的所有线程。