一、互斥锁
线程和线程的同步对象(互斥量,读写锁,条件变量)都具有属性。在修改属性前都需要对该结构进行初始化。使用后要把该结构回收。我们用pthread_ mutexattr_init函数对pthread_mutexattr结构进行初始化,用pthread_mutexattr_destroy函数对该结构进行回收。
名称:: | pthread_mutexattr_init/pthread_mutexattr_destroy |
功能: | 初始化/回收pthread_mutexattr_t结构 |
头文件: | #include <pthread.h> |
函数原形: | int pthread_mutexattrattr_init(pthread_mutexattr_t *attr); int pthread_mutexattrattr_destroy( pthread_mutexattr_t *attr ); |
参数: | attr pthread_mutexattr_t结构变量 |
返回值: | 若成功返回0,若失败返回错误编号。 |
pthread_mutexattr_init将属性对象的值初始化为缺省值。并分配属性对象占用的内存空间。
attr中pshared属性表示用这个属性对象创建的互斥锁的作用域,它的取值可以是PTHREAD_PROCESS_PRIVATE(缺省值,表示由这个属性对象创建的互斥锁只能在进程内使用)或PTHREAD_PROCESS_SHARED。
互斥量属性分为共享互斥量属性和类型互斥量属性。两种属性分别由不同的函数得到并由不同的函数进行修改。pthread_mutexattr_getpshared和pthread_mutexattr_setpshared函数可以获得和修改共享互斥量属性。pthread_mutexattr_gettype和pthread_mutexattr_settype函数可以获得和修改类型互斥量属性。下面我们分别介绍。
名称:: | pthread_mutexattr_getpshared/pthread_mutexattr_setpshared |
功能: | 获得/修改共享互斥量属性 |
头文件: | #include <pthread.h> |
函数原形: | int pthread_mutexattrattr_ getpshared ( const pthread_attr_t *restrict attr,int*restrict pshared); int pthread_mutexattrattr_ setpshared ( const pthread_attr_t *restrict attr,int pshared); |
参数: |
|
返回值: | 若成功返回0,若失败返回错误编号。 |
共享互斥量属性用于规定互斥锁的作用域。互斥锁的域可以是进程内的也可以是进程间的。pthread_mutexattrattr_ getpshared可以返回属性对象的互斥锁作用域属性。可以是以下值:PTHREAD_PROCESS_SHARED,PTHREAD_PROCESS_PRIVATE。如果互斥锁属性对象的pshared属性被置PTHREAD_PROCESS_SHARED。那么由这个属性对象创建的互斥锁将被保存在共享内存中,可以被多个进程中的线程共享。如果pshared属性被置为PTHREAD_PROCESS_PRIVATE,那么只有和创建这个互斥锁的线程在同一个进程中的线程才能访问这个互斥锁。
名称:: | pthread_mutexattr_gettype/pthread_mutexattr_settype |
功能: | 获得/修改类型互斥量属性 |
头文件: | #include <pthread.h> |
函数原形: | |
参数: |
|
返回值: | 若成功返回0,若失败返回错误编号。 |
pthread_mutexattr_gettype函数可以获得互斥锁类型属性。缺省的互斥锁类型属性是PTHREAD_MUTEX_DEFAULT。
合法的类型属性值有:
PTHREAD_MUTEX_NORMAL;
PTHREAD_MUTEX_ERRORCHECK;
PTHREAD_MUTEX_RECURSIVE;
PTHREAD_MUTEX_DEFAULT。
类型说明:
PTHREAD_MUTEX_NORMAL
这种类型的互斥锁不会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会引起这个线程的死锁。如果试图解锁一个由别的线程锁定的互斥锁会引发不可预料的结果。如果一个线程试图解锁已经被解锁的互斥锁也会引发不可预料的结果。
PTHREAD_MUTEX_ERRORCHECK
这种类型的互斥锁会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会返回一个错误代码。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码。
PTHREAD_MUTEX_RECURSIVE
如果一个线程对这种类型的互斥锁重复上锁,不会引起死锁,一个线程对这类互斥锁的多次重复上锁必须由这个线程来重复相同数量的解锁,这样才能解开这个互斥锁,别的线程才能得到这个互斥锁。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码。这种类型的互斥锁只能是进程私有的(作用域属性为PTHREAD_PROCESS_PRIVATE)。
PTHREAD_MUTEX_DEFAULT
这种类型的互斥锁不会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会引起不可预料的结果。如果试图解锁一个由别的线程锁定的互斥锁会引发不可预料的结果。如果一个线程试图解锁已经被解锁的互斥锁也会引发不可预料的结果。POSIX标准规定,对于某一具体的实现,可以把这种类型的互斥锁定义为其他类型的互斥锁。
应用互斥量需要注意的几点
1、互斥量需要时间来加锁和解锁。锁住较少互斥量的程序通常运行得更快。所以,互斥量应该尽量少,够用即可,每个互斥量保护的区域应则尽量大。
2、互斥量的本质是串行执行。如果很多线程需要领繁地加锁同一个互斥量,
则线程的大部分时间就会在等待,这对性能是有害的。如果互斥量保护的数据(或代码)包含彼此无关的片段,则可以特大的互斥量分解为几个小的互斥量来提高性能。这样,任意时刻需要小互斥量的线程减少,线程等待时间就会减少。所以,互斥量应该足够多(到有意义的地步),每个互斥量保护的区域则应尽量的少。
二、自旋锁
1 Pthread mutex
Mutex属于sleep-waiting类型的锁. 从 2.6.x 系列稳定版内核开始, Linux 的 mutex 都是 futex (Fast-Usermode-muTEX)锁.
futex(快速用户区互斥的简称)是一个在Linux上实现锁定和构建高级抽象锁如信号量和POSIX互斥的基本工具。它们第一次出现在内核开发的2.5.7版;其语义在2.5.40固定下来,然后在2.6.x系列稳定版内核中出现。
Futex 是由Hubertus Franke(IBM Thomas J. Watson 研究中心), Matthew Kirkwood,Ingo Molnar(Red Hat)和 Rusty Russell(IBM Linux 技术中心)等人创建的。
Futex 是由用户空间的一个对齐的整型变量和附在其上的内核空间等待队列构成. 多进程或多线程绝大多数情况下对位于用户空间的futex 的整型变量进行操作(汇编语言调用CPU提供的原子操作指令来增加或减少),而其它情况下,则需要通过代价较大的系统调用来对位于内核空间的等待队列进行操作(如唤醒等待的进程/线程,或 将当前进程/线程放入等待队列). 除了多个线程同时竞争锁的少数情况外,基于 futex 的 lock 操作是不需要进行代价昂贵的系统调用操作的.
.
这种机制的核心思想是通过将大多数情况下非同时竞争 lock 的操作放到在用户空间来执行,而不是代价昂贵的内核系统调用方式来执行,从而提高了效率.
Pthreads提供的Mutex锁操作相关的API主要有:
1、 pthread_mutex_lock (pthread_mutex_t *mutex);
2、 pthread_mutex_trylock (pthread_mutex_t *mutex);
3、 pthread_mutex_unlock (pthread_mutex_t *mutex);
2 Pthread spinlock
spinlock,也称自旋锁,是属于busy-waiting类型的锁.在多处理器环境中, 自旋锁最多只能被一个可执行线程持有。如果一个可执行线程试图获得一个被争用(已经被持有的)自旋锁,那么该线程就会一直进行忙等待,自旋,也就是空转,等待锁重新可用。如果锁未被争用,请求锁的执行线程便立刻得到它,继续执行。
一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋,特别的浪费CPU时间,所以自旋锁不应该被长时间的持有。实际上,这就是自旋锁的设计初衷,在短时间内进行轻量级加锁。
Kernel中的自旋锁不能够在能够导致睡眠的环境中使用。举个例子,一个线程A获得了自旋锁L;这个时候,发生了中断,在对应的中断处理函数B中,也尝试获得自旋锁L,就会中断处理程序进行自旋。但是原先锁的持有者只有在中断处理程序结束后,采用机会释放自旋锁,从而导致死锁。
由于涉及到多个处理器环境下,spin lock的效率非常重要。因为在等待spin lock的过程,处理器只是不停的循环检查,并不执行其他指令。但即使这样, 一般来说,spinlock的开销还是比进程调度(context switch)少得多。这就是spin lock 被广泛应用在多处理器环境的原因
Pthreads提供的与Spin Lock锁操作相关的API主要有:
pthread_spin_lock (pthread_spinlock_t *lock);
pthread_spin_trylock (pthread_spinlock_t *lock);
pthread_spin_unlock (pthread_spinlock_t *lock);
三、读写锁
1、读写锁的初始化:
定义读写锁: pthread_rwlock_t m_rw_lock;
函数原型: pthread_rwlock_init(pthread_rwlock_t * ,pthread_rwattr_t *);
返回值:0,表示成功,非0为一错误码
2、读写锁的销毁:
函数原型: pthread_rwlock_destroy(pthread_rwlock_t* );
返回值:0,表示成功,非0表示错误码
3、获取读写锁的读锁操作:分为阻塞式获取和非阻塞式获取,如果读写锁由一个写者持有,则读线程会阻塞直至写入者释放读写锁。
阻塞式:
函数原型:pthread_rwlock_rdlock(pthread_rwlock_t*);
非阻塞式:
函数原型:pthread_rwlock_tryrdlock(pthread_rwlock_t*);
返回值: 0,表示成功,非0表示错误码,非阻塞会返回ebusy而不会让线程等待
4、获取读写锁的写锁操作:分为阻塞和非阻塞,如果对应的读写锁被其它写者持有,或者读写锁被读者持有,该线程都会阻塞等待。
阻塞式:
函数原型:pthread_rwlock_wrlock(pthread_rwlock_t*);
非阻塞式:
函数原型:pthread_rwlock_trywrlock(pthread_rwlock_t*);
返回值: 0,表示成功
释放读写锁:
函数原型:pthread_rwlock_unlock(pthread_rwlock_t*);
互斥锁与读写锁的区别:
当访问临界区资源时(访问的含义包括所有的操作:读和写),需要上互斥锁;
当对数据(互斥锁中的临界区资源)进行读取时,需要上读取锁,当对数据进行写入时,需要上写入锁。
读写锁的优点:
对于读数据比修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都要上互斥锁,而采用读写锁,则可以在任一时刻允许多个读出者存在,提高了更高的并发度,同时在某个写入者修改数据期间保护该数据,以免任何其它读出者或写入者的干扰。
读写锁描述:
获取一个读写锁用于读称为共享锁,获取一个读写锁用于写称为独占锁,因此这种对于某个给定资源的共享访问也称为共享-独占上锁。
有关这种类型问题(多个读出者和一个写入者)的其它说法有读出者与写入者问题以及多读出者-单写入者锁。
四、条件变量
1 -- 条件变量是同步线程的一种机制,它允许线程挂起,让出处理器等待其他线程向它发送信号,该线程收到该信号后被唤醒继续执行程序。对条件变量基本的操作就是:a)向条件变量发送信号,唤醒等待的线程;b)等待条件变量并挂起直至其他线程向该条件变量发送信号。为了防止竞争,条件变量总是和一个互斥锁同时使用。
#include <pthread.h> pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr); 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); int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); int pthread_cond_destroy(pthread_cond_t *cond);返回0为成功,非0为失败。
注意:pthread_cond_init,pthread_cond_signal,pthread_cond_broadcast,pthread_cond_wait只返回0,不会返回其他错误码。也就是说这几个函数每次调用都会成功,编程时不用检查返回值。但pthread_cond_timedwait和pthread_cond_destroy会返回错误码,需要注意!
1.静态方式
初始化方法:pthread_cond_t pcond = PTHREAD_COND_INITIALIZER;
对于静态分配的条件变量,如果使用默认的条件变量属性,可以直接使用PTHREAD_COND_INITIALIZER对条件变量进行赋值来初始化。pthread_cond_t是一个结构体,同样PTHREAD_COND_INITIALIZER是一个结构体常量。
2.动态方式
动态方式调用pthread_cond_init函数对条件变量初始化,该函数的第二个参数指向条件变量属性的结构体。尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常设置为NULL。
(1)释放互斥锁 并且 线程挂起(这两个操作是一个原子操作);
(2)线程获得信号,尝试获得互斥锁后被唤醒;
我们知道调用pthread_cond_signal给条件变量发送信号时,如果当时没有线程在等待这个条件变量,信号将被丢弃。如果"释放互斥锁"和"线程挂起"不是一个原子操作,那么pthread_cond_wait线程在"释放互斥锁"和"线程挂起"之间,如果有信号一旦发生,程序就会错失一次条件变量变化。
返回错误码:ETIMEDOUT。如果在指定时刻前,没有信号发生。
返回错误码:EINTR。该函数被信号中断。
返回错误码:EBUSY。当前还有线程在该条件变量上等待。
这里就使用到了条件变量,大体的流程是这样的:
(1)主线程初始化一个条件变量和一个互斥锁;
(2)主线程创建n个工作者线程;
(3)主线程调用pthread_mutex_lock锁定互斥锁,然后调用pthread_cond_wait在条件变量上wait,等待被唤醒;
(4)子线程执行初始化代码,完毕后获取互斥锁,累加已初始化线程数量,调用pthread_cond_signal给该条件变量发送信号,同时释放互斥锁;
(5)线程调度唤醒主线程,主线程检查现在已经初始化的线程数目,如果都初始化了就释放互斥锁,顺序执行其他代码;如果还没初始化完毕,调用pthread_cond_wait再次等待。
主线程执行如下代码:
/*主线程创建nthreads个线程,线程创建后进行初始化,初始化完毕后累加init_count*/ for (i = 0; i < nthreads; i++) { create_worker(worker_libevent, &threads[i]); } /*主线程等待线程全部初始化,条件是已初始化量init_count等于线程数nthreads*/ pthread_mutex_lock(&init_lock); while (init_count < nthreads) { pthread_cond_wait(&init_cond, &init_lock); } pthread_mutex_unlock(&init_lock);工作者线程执行如下代码:
//TODO:初始化代码 pthread_mutex_lock(&init_lock); init_count++; //累加已初始化线程数量 pthread_cond_signal(&init_cond); pthread_mutex_unlock(&init_lock);看完这段代码之后,估计都会有一个疑问: 流程(3)中互斥锁已被主线程获取了,在线程全部初始化完毕之前,主线程并没有显式释放互斥锁,为什么在流程(4)中工作者线程还能获取到互斥锁呢?在讲解pthread_cond_wait函数时说明过这个问题,也可以从下图看出其中奥妙。