1. 概要
线程的同步,发生在多个线程共享相同内存的时候,这时,要保证每个线程在每个时刻看到的共享数据是一致的。如果每个线程使用的变量都是其他线程不会使用的(read & write),或者变量是只读的,就不存在一致性问题。但是,如果两个或两个以上的线程可以read / write一个变量时,就需要对线程进行同步,以确保它们在访问该变量时,不会得到无效的值,同时也可以唯一地修改该变量并使它生效。
以上就是我们所说的线程同步。
线程同步有三种常用的机制:互斥量(muex) ,读写锁(rwlock)和条件变量(cond) 。
互斥量:有两种状态:lock 和 unlock,它确保同一时间只有一个线程访问数据;
读写锁:有三种状态:读加锁,写加锁,不加锁,只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁;
条件变量:给多线程提供了一个会合的场所,与互斥量一起使用时,允许线程以无竞争的方式等待特定条件的发生。
2. 互斥量 (mutex)
互斥量从本质上说就是一把锁,提供对共享资源的保护访问。
(1) 初始化:
在Linux下,线程的互斥量数据类型是pthread_mutex_t。在使用前,要对它进行初始化:
对于静态分配的互斥量,可以把它设置为PTHREAD_MUTEX_INITIALIZER,或者调用pthread_mutex_init。
对于动态分配的互斥量,在申请内存(malloc)之后,通过pthread_mutex_init进行初始化,并且在释放内存(free)前需要调用pthread_mutex_destroy。
原型:
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);
int pthread_mutex_init(pthread_mutex_t* mutex);
头文件:<pthread.h>
返回值:成功则返回0,出错则返回错误编号
说明:如果使用默认的属性初始化互斥量,只需把attr设为NULL,其他值在以后讲解。
(2) 互斥操作
对共享资源的访问,要对互斥量进行加锁,如果互斥量已经上了锁,调用线程会阻塞,直到互斥量被解锁。在完成了对贡献资源的访问后,要对互斥量进行解锁。
首先说一下加锁函数:
头文件:<pthread.h>
原型:
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_tyrlock(pthread_mutex_t* mutex);
返回值:成功则返回0,出错则返回错误编号。
说明:具体说一下trylock函数,这个函数是非阻塞调用模式,也就是说,如果互斥量没被锁住,trylock函数将把互斥量加锁,并获得对共享资源的访问权限;如果互斥量被锁住了,trylock函数将不会阻塞等待而直接返回EBUSY,表示共享资源处于忙状态。
再说一下解锁函数:
头文件:<pthread.h>
原型:
int pthread_mutex_unlock(pthread_mutex_t* mutex);
返回值:成功则返回0,出错则返回错误编号。
(3) 死锁:
死锁主要发生在有多个依赖锁存在时,会在一个线程试图与另一个线程相反顺序锁住互斥量时发生,如何避免死锁是使用互斥量应格外注意的东西。
总体来讲,有几个不成文的基本原则:
对共享资源操作前一定要获得锁;
完成操作以后一定要释放锁;
尽量短时间地占用锁;
如果有多锁,如获得顺序是ABC连环扣,释放顺序也应该是ABC;
线程错误返回时应释放它所获得的锁。
3. 读写锁
读写锁是因为有3种状态,所以有更高的并行性。
(1) 特性:
一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁,正是因为这个特性,
当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须阻塞直到所有线程释放锁。
通常,当读写锁处于读模式锁住状态时,如果有另外线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求,这样可以避免读模式锁长期占用而等待的写模式锁请求长期阻塞。
(2) 适用性:
读写锁适合于对数据结构的读次数比写次数多得多的情况。 因为,读模式锁定时可以共享,以写模式锁住时意味着独占,所以,读写锁又叫共享-独占锁。
(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,出错则返回错误编号。
同互斥量以上,在释放读写锁占用的内存之前,需要先通过pthread_rwlock_destroy对读写锁进行清理工作,释放有init分配的资源。
(4) 读和写:
#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,出错则返回错误编号。
这三个函数分别实现获取读锁,获取写锁和释放锁的操作,获取锁的两个函数是阻塞操作,同样,非阻塞的函数为:
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
成功则返回0,出错则返回错误编号。
非阻塞的获取锁操作,如果可以获取则返回0,否则返回错误的EBUSY。
4. 条件变量
条件变量分为两部分:条件和变量。条件本身是由互斥量保护的。线程在改变条件状态前先要锁住互斥量。
(1) 初始化:
条件变量采用的数据类型是pthread_cond_t,在使用之前必须要进行初始化,这包括两种方式:
静态:可以把常量PTHREAD_COND_INITIALIZER给静态分配的条件变量。
动态:pthread_cond_init函数,释放动态条件变量的内存空间之前,要用pthread_cond_destroy对其进行清理。
#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,会创建一个默认属性的条件变量;非默认情况以后讨论。
(2) 等待条件:
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
int pthread_cond_timewait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex, const struct timespec* restrict timeout);
成功则返回0,出错则返回错误编号。
这两个函数分别是阻塞等待和超时等待。
等待条件函数等待条件变量为真,传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传递给函数,函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子的。这样便关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。
当pthread_cond_wait返回时,互斥量再次被锁住。
(3) 通知条件:
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cont_t* cond);
成功则返回0,出错则返回错误编号。
这两个函数用于通知线程条件已经满足,调用这个两个函数,也称向线程或条件发送信号。必须注意,一定要在改变条件状态以后再给线程发送信号。