1 线程同步概念
假设有有三个线程A,B,C,当前一个线程A对内存中的共享资源进行访问时,其它线程B,C都不可以对这块内存进行操作,直到线程A对这块内存访问完毕为止,B,C中的一个才能访问这块内存,剩余的一个需要继续阻塞等待,以此类推,直到所有的线程都对这块内存操作完毕。线程对内存的这种访问就称为线程同步,通过对概念的介绍,我们可以了解到所谓的线程同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行访问。
1.1 为什么要进行同步
两个线程在数熟的时候需要分时不用CPU时间片,并且测试程序中调用了sleep()导致线程的CPU时间片没有用完就被迫挂起了,这样能让CPU的上下文切换(保存当前状态,下一次运行的时候需要加载保存的状态)更加频繁,更容易再现数据混乱这个现象。
CPU对应寄存器、一级缓存,二级缓存、三级缓存是独占的,用于存储处理的数据和线程的状态信息,数据被CPU处理完成后需要再次写入到物理内存中,物理内存数据也可以通过文件IO操作写入磁盘中。
在测试程序中两个线程共用全局变量counter,当线程变成运行状态后开始数数,从物理内存加载数据,然后将数据放到CPU进行运算,最后i将结果更新到物理内存中。如果数数的两个线程可以顺利完成这个流程,那么得到的结果肯定是正确的。
如果线程A执行这个过程期间失去了CPU,线程A被挂起了,最新的数据没能更新到物理内存。线程B变成运行态之后从物理内存读取数据,很显然它没有拿到最新数据,只能基于旧的数据向后数,然后失去CPU时间片挂起。线程A得到CPU时间片,变成运行态,第一件事就是将上次没有更新的数据更新到内存,但是这样会导致线程B已经更新到内存的数据被覆盖。最终导致有些数据被重复计数了很多次。
1.2 同步方式
对于对各线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁,读写锁,条件变量,信号量,所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区或者堆区变量,这些变量对应的共享资源也被称为临界资源。
1)在临界区代码的上边,添加锁定函数,对临界区上锁:阻止其它线程进入临界区执行,并且阻塞上锁上。
2)在临界区代码的下边,添加解锁函数,对临界区解锁:出临界区的线程会将锁定的那把锁打开,其它抢到锁的线程可以进入临界区。
3)通过锁机制功能保证临界代码最多只能同时有一个线程访问,这样并行访问就变成了串行访问。
2 互斥锁
2.1 互斥锁函数
互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,所有的线程只能顺序执行(不能并行处理),这样多线程访问共享资源数据混乱的问题就被解决了,需要付出的代价是执行效率低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理。
在Linux中互斥锁类型为pthread_mutex_t,创建一个这样类型的变量就得到了一个互斥锁:
pthread_mutex_t mutex;
在创建的锁对象中保存了当前这把锁的状态信息:锁还是打开,此外锁状态还基类了给这把锁加锁的状态信息(线程ID),一个互斥锁变量只能被一个线程锁定,被锁定之后其它线程再对互斥锁加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个互斥锁,锁的个数和线程的个数无关。
Linux提供的互斥锁操作函数如下,如果函数调用成功返回0,调用失败会返回相应的错误号
/*pthread_mutex_destroy, pthread_mutex_init-销毁和初始化一个互斥锁。- 销毁和初始化一个互斥锁
*/#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
/*
描述:pthread_mutex_destroy()函数应该销毁由mutex引用的互斥锁对象;这个mutex对象实际上变为未被初始化。一种实现可能使得pthread_mutex_destroy()设置mutex引用的对象未一个无效值。
可以使用 pthread_mutex_init()重新初始化一个被销毁的mutex对象;在一个对象已经被销毁后引用它的结果是未定义的。
销毁一个未被上锁的初始化过的mutex应该是安全的。尝试销毁一个上锁的mutex导致未定义的行为。pthread_mutex_init()函数应该初始化其属性由attr指定的mutex引用的mutex。如果attr是NULL,默认的mutex属性被使用;作用应该与传递一个默认mutex属性对象相同。
成功初始化后,mutex的状态变成了已初始化和未上锁。仅mutex自身可以用作执行同步。在pthread_mutex_lock(), pthread_mutex_trylock(), pthread_mutex_unlock()和pthread_mutex_destroy()的调用种引用mutex的副本的结果是未定义的。
尝试初始化一个已初始化的mutex结果导致未定义行为。在默认互斥锁属性适合的情况下,宏PTHREAD_MUTEX_INITIALIZER可以用于初始化静态被分配的互斥锁。
作用除了其不执行错误外,等价于用指定为NULL的参数attr调用pthread_mutex_init()的动态分配。返回值:如果成功pthread_mutex_destroy()和pthread_mutex_init()应该返回0;否则,返回一个标识错误的错误代码。[EBUSY]和[EINVAL]错误检查,如果执行,就像在此函数执行开始时它们被立即执行并且应该在修改mutex执行的mutex状态前产生一个错误返回。错误:
如果以下,pthread_mutex_destroy() 会出错:EBUSY:执行发现尝试销毁这样的互斥锁对象,它被上锁或者被另一个线程被引用时(例如在用在了pthread_cond_timedwait()或pthread_cond_wait())。EINVAL :mutex指定的值无效如果以下pthread_mutex_init()会出错:EAGAIN :系统缺少必要的资源(除了内存外)来初始化另一个互斥锁。ENOMEM:初始化这个互斥锁的内存不足 IEPERM :调用者没有执行这个操作的权限。 EBUSY :执行发现尝试重新初始化由mutex引用的对象,它先前被初始化了,但还未被销毁。EINVAL :由attr指向的值无效
这些函数不应该返回一个[EINTR]的错误代码。
*/
/*pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock锁定和解锁一个mutex
*/
#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);
/*描述:通过调用pthread_mutex_lock()应该锁定由mutex引用的mutex对象。如果这个mutex已经被锁定,调用线程在此mutex可以获取前应该阻塞。
这个操作应该以由mutex引用的mutex对象中用处于调用线程作为其所有者的锁定状态返回。如果mutex类型是PTHREAD_MUTEX_NORMAL,不提供死锁检查。尝试再次锁定这个mutex造成死锁。如果一个线程尝试解锁还未被锁定的mutex或者处于解锁的mutex,未定义的行为结果。
如果mutex类型是PTHREAD_MUTEX_ERRORCHECK,则提供错误检查。如果一个线程尝试再次锁定一个已经被锁定的mutex,应该返回一个错误。如果一个线程尝试解锁一个还未被锁定的mutex或者处于解锁的mutex
应该返回一个错误。如果mutex类型是 PTHREAD_MUTEX_RECURSIVE,则mutex应该维护一个锁计数的概念。当一个线程成功地第一次获取一个mutex,锁计数应该设为0。每次一个线程再次锁定这个mutex,应该增加锁计数1.
每次线程解锁这个mutex,锁计数应该减1。当锁计数达到0,这个mutex应该变成其它线程可获取。如果一个线程尝试解锁一个还未被锁定的mutex或者处于解锁的mutex
应该返回一个错误。
如果mutex类型是PTHREAD_MUTEX_DEFAULT,尝试递归锁定这个mutex导致未定义的行为。尝试解锁一个未被调用线程锁定的mutex导致未定义的行为。如果mutex未被锁定,尝试解锁这个mutex,导致未定义的行为。pthread_mutex_trylock() 函数应该等价于pthread_mutex_lock(),除了如果由mutex引用的mutex对象当前被(任何线程,包括当前线程)锁定,调用应该立即返回。
如果mutex类型是 PTHREAD_MUTEX_RECURSIVE并且这个mutex当前被调用线程所有,mutex锁计数应该加1并且 pthread_mutex_trylock()函数应该给立即返回成功。
pthread_mutex_unlock() 函数应该释放由mutex引用的mutex对象。释放一个mutex的方式取决于mutex的类型属性。当调用 pthread_mutex_unlock() 时,
如果有线程阻塞在由mutex引用的mutex对象,导致mutex变得可用,调度策略应该确定哪个线程应该获取这个mutex。(在PTHREAD_MUTEX_RECURSIVE mutexes的情况中,mutex应该在计数达到0时变得可用并且调用线程不在在此mutex上有任何锁)
如果一个信号被发送给一个等待mutex的线程,从信号handler返回时,这个线程应该继续等待这个mutex,就像它未被中断。
返回值:如果成功, pthread_mutex_lock() 和 pthread_mutex_unlock()函数应该返回0,否则返回一个表示错误的错误代码。
RETURN VALUE如果mutex引用的mutex对象被获取了,pthread_mutex_trylock()函数应该返回0。否则,返回一个表示错误的错误代码。错误:如果以下,pthread_mutex_lock() 和 pthread_mutex_trylock() 函数应该出错:EINVAL: 用值为PTHREAD_PRIO_PROTECT的协议属性创建了互斥锁,并且调用线程的优先级高于mutex的当前优先级上限。 如果以下,pthread_mutex_trylock()会出错EBUSY:因为互斥锁以被锁定,不能获取它。如果以下, pthread_mutex_lock(), pthread_mutex_trylock(), 和 pthread_mutex_unlock()会出错EINVAL : 由mutex指定的值未指向一个已经初始化的mutex对象。EAGAIN:因为已经超过了递归锁的最大数目,不能获取这个mutex如果以下pthread_mutex_lock()函数会出错:EDEADLK:当前线程已经拥有了这个mutex如果以下,mutex_unlock()函数会出错EPERM:当前线程不拥有这个mutex 这些函数不应该返回[EINTR]错误代码*/
以下时线程同步的示例,每个线程都进行50次数数:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>int number = 0;
#define MAX 50pthread_mutex_t mutex;void * subThread(void * arg)
{char * name = (char *) arg;int i;for (i = 0; i < MAX; i++){pthread_mutex_lock(&mutex);int curr = number;curr++;usleep(10);number = curr;pthread_mutex_unlock(&mutex);usleep(20);printf("Thread-%s, id = %lu\n", name, pthread_self());}return NULL;
}int main(int argc, char *argv[])
{int num = argc - 1;int i;pthread_t *ptid;pthread_mutex_init(&mutex, NULL);printf("main-thread is start ...\n");ptid = (pthread_t *)malloc((argc-1) * sizeof(pthread_t));if (ptid == NULL){perror("malloc");exit(EXIT_FAILURE);}for (i = 0; i < num; i++){pthread_create(&ptid[i], NULL, subThread, (void *)argv[i+1]);}for (i = 0; i < num; i++){pthread_join(ptid[i], NULL);}free(ptid);pthread_mutex_destroy(&mutex);printf("subThread number: %d, count to %d\n", num, number);printf("main-thread is completed\n");return 0;
}
以下程序执行结果如下,进行线程同步后,3个线程共数数150次:
(base) blctrl@blctrl-s2:~/C_Program/build-CProgram-Desktop-Debug$ ./CProgram Tom Jerry TT
main-thread is start ...
Thread-Tom, id = 128620985058880
Thread-TT, id = 128620964087360
Thread-Tom, id = 128620985058880
Thread-Jerry, id = 128620974573120
...
Thread-Tom, id = 128620985058880
Thread-Tom, id = 128620985058880
subThread number: 3, count to 150
main-thread is completed
3 死锁
当多个线程访问共享资源时,需要加锁,如果锁使用不当,就会造成死锁这种现场。如果现场死锁,造成的后果是所有的线程都被阻塞,并且线程的阻塞是无法解开的(因为可以解锁的线程也被阻塞了)。
造成死锁的场景有以下几种:
1)加锁之后忘记解锁了
2)重复加锁,造成死锁
3)在程序中,有多个共享资源,因此有很多把锁,随意加锁,导致相互被阻塞。
在使用多线程编程时,如何避免死锁呢?
1)避免多次锁定,多检查
2)对共享资源访问完毕后,一定要解锁,或者在加锁时使用trylock
3) 如果程序中有多把锁,可以控制对锁的访问顺序(顺序访问共享资源,但是有些情况下是做不到的),另外可以在对其它互斥锁加锁操作之前,先释放当前线程拥有的互斥锁。
4)项目程序中引入一些专门用于死锁检测的模块。
4 读写锁
4.1 读写锁函数
读写锁是互斥锁的升级版,在进行读操作的时候可以提高程序的执行效率,如果所有线程都是进行读操作,那么读是并行的,但是使用互斥锁,读操作也是串行的。
读写锁是一把锁,锁的类型为pthread_rwlock_t,有了类型之后就可以创建一把互斥锁:
pthread_rwlock_t rwlock;
之所以称其为读写锁,是因为这把锁既可以锁定读操作,也可以锁定写操作。为了方便理解,可以大致认为这把锁中记录了这些信息:
1)锁的状态:锁定/打开
2)锁定的是什么操作:读操作/写操作,使用读写锁锁定了读操作,需要先解锁才能去锁定写操作,反之亦然。
3)哪个线程将这把锁锁上。
读写锁的使用方式与互斥锁的使用方式完全相同:找共享资源,确定临界区,在临界区的开始位置加锁(读锁/写锁),临界区的结束位置解锁。
因为通过一把读写锁可以锁定读或者写操作,下面介绍以下关于读写锁的特点:
1)使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读读是共享的。
2)使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的。
3)使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问这两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。
如果说程序中所有的线程对共享资源做写操作,使用读写锁没有优势,和互斥锁是一样的,如果说程序中所有线程都对共享资源有写也有读操作,并且对共享资源读的操作越多,读写锁更有优势。
Linux提供的读写操作原型如下,如果函数调用成功返回0,失败返回对应的错误号:
/*pthread_rwlock_destroy, pthread_rwlock_init - 销毁和初始化一个读写锁对象
*/
#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);
/*
描述:pthread_rwlock_destroy() 函数应该销毁rwlock引用的读写锁对象并且释放这个锁使用的任何资源。
在另一个线程调用pthread_rwlock_init()再次初始化这个锁前,后续使用这个锁rwlock的作用是未定义的。
执行会使得pthread_rwlock_destroy() 设置由rwlock引用的对象为一个无效的值。如果在任何线程持有rwlock时调用pthread_rwlock_destroy() ,
结果是未定义的。尝试销毁一个未初始化的读写锁导致未定义的行为。pthread_rwlock_init()函数应该分配使用rwlock引用的读写锁所需的任何资源并且用attr引用的属性初始化这个锁为一个未锁定的状态。
如果attr是NULL,应该使用默认读写锁属性。作用与传递要给默认读写锁属性对象的地址相同。一旦初始化,
锁可以被使用任何次数,而不需要被再次初始化。如果指定一个已经初始化的读写锁调用pthread_rwlock_init(),结果是未定义的。
如果使用没有首先被初始化的读写锁,结果是未定义的。如果pthread_rwlock_init() 函数出错,rwlock应该未被初始化并且rwlock的内容未被定义。
仅rwlock引用的对象才会被用于执行同步。在pthread_rwlock_destroy(),pthread_rwlock_rdlock(), pthread_rwlock_timedrdlock(), pthread_rwlock_timedwrlock(),pthread_rwlock_tryrdlock(), pthread_rwlock_trywrlock(), pthread_rwlock_unlock(), 或pthread_rwlock_wrlock()调用中引用了那个对象的副本,结果是未定义的。返回值:成功,pthread_rwlock_destroy() 和 pthread_rwlock_init()函数返回0;否则,将返回指示错误的一个错误代码The [EBUSY] 和 [EINVAL] 错误检查,如果执行,就像,此函数运行开始时立即被执行并且在修改rwlock指定的读写锁的状态前产生一个错误。错误:如果以下,pthread_rwlock_destroy()函数会出错EBUSY :执行发生尝试在其被锁定时产生销毁rwlock引用的对象。EINVAL:rwlock指定的值无效 如果以下,pthread_rwlock_init()函数会出错:EAGAIN:系统缺少初始化另一个读写锁所需的必要资源(除了内存外) ENOMEM :初始化读写锁的内存不足EPERM:调用者没有执行此操作的权限 如果以下, pthread_rwlock_init()函数会出错:EBUSY :执行已经探测到尝试再次初始化由rwlock引用的对象,其是先前已经被初始化但还未被销毁的读写锁。EINVAL :attr指定的值无效。这些函数不应该返回[EINTR]错误代码。*/
/*pthread_rwlock_rdlock, pthread_rwlock_tryrdlock:锁定一个读写锁用于读取
*/
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);/*
描述:pthread_rwlock_rdlock()函数应该使用一个读写锁锁定由rwlock引用的读写锁。如果写程序没有持有这个锁并且没有写程序阻塞在这个锁上,
调用线程获取这个读写锁。
支持线程执行调度选项,并且与这个锁相关的线程正在以调度策略SCHED_FIFO或SCHED_RR执行,如果写程序持有这个锁或者更高或者相同优先级的写程序被阻塞在这个锁上,
调用线程获取不到这个锁;否则,调用线程应该获取这个锁。
支持线程执行调度策略选项,并且与这个锁相关的线程正在以SCHED_SPORADIC调度策略执行,如果一个写程序持有这个锁或者如果更高或相同优先级的写程序阻塞在这个锁上,
调用线程获取不到这个锁;否则调用线程应该获取这个锁。如果不支持线程执行策略选项,当写程序没有持有锁并且有写程序阻塞于此锁上,调用线程是否获取锁是实现定义的。如果一个写程序持有锁,调用线程获取不到读锁。
如果读锁未被获取,调用线程在其能获取这个锁前应该阻塞。如果在进行调用时,它持有一个写锁,调用线程会死锁。一个线程可能持有多个在rwlock上的并发读锁(即是,成功地调用pthread_rwlock_rdlock() 多次)。如果这样,程序应该确保线程执行匹配地解锁(即是,它调用 pthread_rwlock_unlock() n次)。实现确保的最大数目的同时读锁应用于读写锁应该是实现定义的。如果超过这个最大数目,pthread_rwlock_rdlock() 函数会出错:pthread_rwlock_tryrdlock() 函数像pthread_rwlock_rdlock() 函数实现一个读锁,除了在pthread_rwlock_rdlock()调用阻塞调用线程,此函数应该出错。
任何情况下, pthread_rwlock_tryrdlock()不阻塞;它中使要么获取锁或者失败并且立即返回。如果用一个未初始化的读写锁调用这些函数,结果是未定义的。如果信号被发送给一个等待读写锁用于读取的线程,在从信号handler返回时,此线程继续等待这个用于读取的读写锁,就像其没有被中断。
错误:如果以下, pthread_rwlock_tryrdlock()函数应该出错:EBUSY :因为写程序持有这个锁,或者具有合适优先级的写程序被阻塞在其上时,获取不到读写锁用于读取。The pthread_rwlock_rdlock() 和 pthread_rwlock_tryrdlock() functions may fail if:
如果以下, pthread_rwlock_rdlock() 和 pthread_rwlock_tryrdlock()函数应该出错:EINVAL:由rwlock指定的值没有指向一个初始化过的读写锁对象。EAGAIN:因为已经超过了rwlock读锁的最大次数,获取不到读锁。如果以下, pthread_rwlock_rdlock() function会出错:EDEADLK:当前线程已经拥有了用于写的读写锁。这些函数不应该返回错误代码[EINTR]。
*/
/*pthread_rwlock_trywrlock, pthread_rwlock_wrlock -锁定一个读写锁用于写。
*/
#include <pthread.h>int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);/*
描述: pthread_rwlock_trywrlock()函数像pthread_rwlock_wrlock() 函数使用一个写锁,除了如果任何线程当前持有rwlock(用于读或写),此函数应该出错。pthread_rwlock_wrlock()函数应该对由rwlock引用的读写锁使用一个写锁。如果没有其它线程(读程序或写程序)持有读写锁rwlock,调用线程获取这个锁。
否则,在它获取这个锁前,此线程阻塞。如果在其持有读写锁(无论读或写锁)时进行调用,调用线程会死锁。实现会优先写程序避免写饥饿。
如果用未初始化读写锁调用任何这些函数,结果是未定义的。如果一个信号发送给正在等待读写锁用于写的线程,在从信号handler返回时,此线程继续等待读写锁用于写,就像其未被中断。返回值:如果获取了rwlock引用的读写锁对象,pthread_rwlock_trywrlock()应该返回0。否则,返回一个表示错误的错误代码。
如果成功, pthread_rwlock_wrlock()函数应该返回0;否则返回一个表示错误的代码。错误:如果以下, pthread_rwlock_trywrlock() 函数出错:EBUSY :因为它已经被锁定用于读或写,不能获取读写锁。如果以下, pthread_rwlock_trywrlock() 和 pthread_rwlock_wrlock()函数出错:EINVAL:rwlock指定的值指向了一个未被初始化的读写锁对象。如果以下, pthread_rwlock_wrlock() 函数会出错:EDEADLK:当前线程已经拥有了这个读写锁用于写或读。这些函数不应该返回一个[EINTER]的错误代码。
*/
/*pthread_rwlock_unlock - 解锁一个读写锁对象。
*/
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);/*
描述:pthread_rwlock_unlock()函数应该释放一个由rwlock引用的读写锁对象上持有的锁。如果读写锁未被调用线程持有,结果是未定义的。如果调用这个函数从读写锁对象释放一个读锁,而有其它读锁当前持有在这个读写锁对象上,读写锁对象保持在读锁定状态。如果这个函数释放了这个读写锁对象的最后的读锁,
读写锁对象应该处于没有所有者的未锁定状态。如果这个函数被调用释放这个读写锁对象的写锁,读写锁对象应该处于未锁定状态。在其变得可获取时,如果有线程阻塞在这个锁上,调度策略应该确定哪个线程获取这个锁。如果支持线程执行调度策略,当以调度策略SCHED_FIFO, SCHED_RR, 或SCHED_SPORADIC执行的线程正在等待这个锁时,当锁变得可获取时,它们按优先级获取这个锁。对于相同优先级线程,写锁定优先于读锁。
如果不支持线程执行调度,写锁是否优先于读锁是执行定义的。如果用未初始化的读写锁调用这些函数时,结果是未定义的。返回值:如果成功,pthread_rwlock_unlock()应该返回0,否则,返回表示错误的错误代码。错误:如果以下,pthread_rwlock_unlock()函数会出错:EINVAL:由rwlock指定的值没有指向一个已经初始化的读写锁对象 TEPERM:当前线程没有持有读写锁上的锁。 pthread_rwlock_unlock()不应该返回错误代码 [EINTR].
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>pthread_rwlock_t rwlock;
static int number;
static int MAX = 50;void * thread_read(void * arg)
{int num;char * name = (char *)arg;for (int i = 0; i < MAX; i++){pthread_rwlock_rdlock(&rwlock);num = number;printf("thread_read: %s, id:%lu value: %d\n", name, pthread_self(), num);pthread_rwlock_unlock(&rwlock);usleep(rand() % 10);}return NULL;
}void * thread_write(void * arg)
{int num;char * name = (char *)arg;for (int i = 0; i < MAX; i++){pthread_rwlock_wrlock(&rwlock);num = number;num++;number = num;printf("thread_write: %s, id:%lu value: %d\n", name, pthread_self(), num);pthread_rwlock_unlock(&rwlock);}usleep(50);return NULL;
}int main()
{pthread_t wtid[3];pthread_t rtid[5];int i;// 启动3个写线程printf("Start Writer...\n");for (i = 0; i < 3; i++){pthread_create(&wtid[i], NULL, thread_write, (void *)"Writer");}// 启动5个读线程printf("Start Reader...\n");for (i = 0; i < 5; i++){pthread_create(&rtid[i], NULL, thread_read, (void *)"Reader");}for (i = 0; i < 3; i++){pthread_join(wtid[i], NULL);}for (i = 0; i < 5; i++){pthread_join(rtid[i], NULL);}printf("Program finished...\n");return 0;
}
执行结果如下:
thread_write: Writer, id:137211657848384 value: 1
thread_write: Writer, id:137211657848384 value: 2
thread_write: Writer, id:137211657848384 value: 3
...
thread_write: Writer, id:137211647362624 value: 149
thread_write: Writer, id:137211647362624 value: 150
Program finished...
5 条件变量
5.1 条件变量函数
严格意义上说,条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。如果在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞都能阻塞线程,但二者的效果不是一样的,二者的区别如下:
1)假如有0-9 10个线程,这10个线程共同访问同一把互斥锁,如果线程0加锁成功,那么其余1-9线程访问互斥锁都阻塞,所有的线程智能顺序访问临界区。
2)条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。
一般情况下,条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。条件变量类型对应的类型为pthread_cond_t,这样就可以定义一个条件类型的变量了:
pthread_cond_t cond;
被条件变量阻塞的线程的线程信息会被记录到这个变量中,以便在解除阻塞的时候使用。
条件变量操作函数原型如下:
/*pthread_cond_destroy, pthread_cond_init销毁和初始化条件变量。
*/
#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);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;/*
描述:pthread_cond_destroy() 函数应该销毁cond指定的条件变量;对象实际上变成未初始化。执行会使得pthread_cond_destroy()设置cond引用的对象为一个无效值。
使用pthread_cond_init()可以再次初始化一个已经销魂的条件变量;在其已经被销毁后引用这个对象的后果是未定义的。销毁一个已经初始化的在其上没有受阻塞线程的条件变量应该是安全的。尝试销毁一个在其上有受阻塞的其它线程的条件变量导致未定义的行为。pthread_cond_init()函数应该初始化由attr引用属性的cond引用的条件变量。如果attr是NULL,应该使用默认条件变量属性;作用与传递一个默认条件变量属性对象的地址相同。
成功初始化时,条件变量的状态应该变为已经初始化。仅cond自身可以用于执行同步。在pthread_cond_wait(), pthread_cond_timedwait(), pthread_cond_signal(), pthread_cond_broadcast()调用中引用cond的结果是未定义的。
尝试初始化一个已经初始化的条件变量导致未定义行为。在默认条件变量属性合适的情况中,宏PTHREAD_COND_INITIALIZER可以用于初始化被静态分配的条件变量。作用应该等价于参数attr指定为NULL的 pthread_cond_init()的调用的动态初始化相同,
除了不执行错误检查。返回值:如果成功,pthread_cond_destroy() 和 pthread_cond_init()函数应该返回0,否则,应该返回一个错误代码来表示这个错误。如果执行了[EBUSY] 和[EINVAL]错误检查,应该表现为在此函数执行开始执行了它们,并且在修改cond指定的条件变量状态前产生一个错误。错误:
如果以下,pthread_cond_destroy()函数发生错误:EBUSY :执行发现在其被另一个线程引用时尝试消息cond引用的对象(例如,当在pthread_cond_wait() 或pthread_cond_timedwait(),当在pthread_cond_wait() 或pthread_cond_timedwait()中被使用)EINVAL :cond指定的值无效.如果以下, pthread_cond_init() 函数发生错误:EAGAIN :系统缺少初始化另一个条件变量所需的资源(除内存外ENOMEM:初始化条件变量内存不足如果以下, pthread_cond_init() 函数发生错误:EBUSY :执行发现尝试再次初始化cond引用的对象,一个先前初始化的,但还未被销毁的条件变量。EINVAL :attr指定的值无效。这些函数不应该返回[EINTR]错误代码。
*/
示例:在所有阻塞在其上的线程被唤醒后,条件变量可以立即被销毁。例如,考虑以下代码:
struct list {pthread_mutex_t lm;...
}struct elt {key k;int busy;pthread_cond_t notbusy;...
}/* 找一个列表元素并且返回指向其的指针 */
struct elt * list_find(struct list *lp, key k)
{struct elt *ep;pthread_mutex_lock(&lp->lm);while ((ep = find_elt(l, k) != NULL) && ep->busy)/* 等待条件变量时,会释放互斥锁,条件变量出现,则需要获取互斥锁,才能向下执行 */pthread_cond_wait(&ep->notbusy, &lp->lm);if (ep != NULL)ep->busy = 1;pthread_mutex_unlock(&lp->lm);return(ep);
}delete_elt(struct list *lp, struct elt *ep)
{pthread_mutex_lock(&lp->lm);assert(ep->busy);... remove ep from list ...ep->busy = 0; /* Paranoid. */(A) pthread_cond_broadcast(&ep->notbusy);pthread_mutex_unlock(&lp->lm);(B) pthread_cond_destroy(&rp->notbusy);free(ep);
}
在本例中,在等待其的所有线程被唤醒(行A)后,条件变量和其列表元素可以立即被释放(B行),由于互斥锁和代码确保其它线程不会触碰要被删除的元素。
/*pthread_cond_timedwait, pthread_cond_wait -等待一个条件
*/#include <pthread.h>
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_timedwait()和pthread_cond_wait()函数应该阻塞在一个条件变量。应该用调用线程锁定的mutex调用它们,否则未定义行为的结果。这些函数自动释放mutex并且使得调用线程阻塞在条件变量cond上;自动这里表示"自动地让另一个线程访问这个mutex以及这个条件变量"。即,如果另一个线程
能够在即将阻塞线程已经释放它后获取这个mutex,则在那个线程中之后调用pthread_cond_broadcast() 或 pthread_cond_signal()应该作用就像在即将阻塞线程阻塞后发送它。在成功返回时,mutex应该已经被锁定并且应该被调用线程所有。在使用条件变量时,总是有一个布尔预测,涉及与每个条件等待相关联的共享变量,如果这个线程应该继续运行,其为真。从pthread_cond_timedwait() 和 pthread_cond_wait()函数的假醒可能发生。由于从pthread_cond_timedwait() 或 pthread_cond_wait()的返回不隐含有关这个预测值的任何东西,遇到这种返回,应该重新评估这个预测。在相同条件变量上,对并发的pthread_cond_timedwait() 或 pthread_cond_wait() 操作使用多个mutex的作用是未定义的;即,当一个线程等待这个条件变量时,此条件变量绑定到了一个唯一的mutex,在等待返回时,这个(动态)绑定应该结束。一个条件等待(无论是否超时)是一个取消点。当一个线程的取消使能状态设为了PTHREAD_CANCEL_DEFERRED,在处于条件等待时,响应取消请求的副作用是在调用第一个取消点清理处理程序前mutex被(实际上)再次获取。作用就像这个线程未被阻塞,允许执行到从 pthread_cond_timedwait() 或 pthread_cond_wait()返回,但到此,取消点请求以及不是返回到pthread_cond_timedwait() 或 pthread_cond_wait()的调用者,开始了线程取消操作,者包括了调用取消点清理请求程序。当在pthread_cond_timedwait() 或 pthread_cond_wait()调用中被阻塞时,因为它已经被取消,已经解除阻塞的线程不应该消耗任何条件变量,如果有其它线程阻塞在这个条件变量上,可能被并发地定向到这个条件变量。pthread_cond_timedwait()函数应该相当于pthread_cond_wait(),除了如果在发出或广播这个条件变量前,由abstime指定的绝对时间耗尽,或者由abstime指定的绝对时间在这个调用时已经被耗尽,返回一个错误。支持时钟选择选项,条件变量应该有一个时钟属性,它指定了应该被使用来测量由abstime参数的时间的时钟。当这样超时发生时,pthread_cond_timedwait()应该无论如何释放和再次获取由mutex引用的锁。pthread_cond_timedwait()函数也是一个取消点。如果一个信号被发送给等待一个条件变量的线程,在从信号处理程序返回时,线程继续等待这个条件变量,就像它未被中断,或者它应该根据假醒,返回0.返回值:除了遇到 [ETIMEDOUT],所有这些错误检查应该作用就如这个函数执行开始时立即执行它们,并且实际上,在修改由mutex指定的mutex和由cond指定的条件变量的状态前,应该产生一个错误返回。成功完成,应该返回一个0值,否则,返回一个表示错误的代码。错误:如果以下,pthread_cond_timedwait()函数出错:ETIMEDOUT:传给pthread_cond_timedwait()的abstime指定的时间已经耗尽。如果以下,pthread_cond_timedwait() and pthread_cond_wait() 函数会出错:EINVAL :cond, mutex, 或 abstime指定的值无效.EINVAL :为在相同条件变量上的 pthread_cond_timedwait() or pthread_cond_wait() 操作提供了不同的mutex。EPERM :在此调用时,这个mutex不由当前线程T所有。这些函数不应该返回错误代码[EINTR]
*/
/*名称:pthread_cond_broadcast, pthread_cond_signal - 广播和发送条件变量
*/
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);/*
描述:这些函数应该解锁阻塞在一个条件变量上的线程。pthread_cond_broadcast() 函数应该解锁当前阻塞在指定条件变量cond上的所有线程(如果任何线程被阻塞在这个cond上)。pthread_cond_signal()函数应该解锁至少一个线程,它们阻塞在指定的条件变量cond上(如果任何线程阻塞在cond上)如果多个线程被阻塞在一个条件变量上,调度策略应该确定用何种顺序解锁线程。当每个线程作为由pthread_cond_broadcast()或pthread_cond_signal()结果从其对pthread_cond_wait()或pthread_cond_timedwait()的调用返回时,这个线程应该拥有用其调用了pthread_cond_wait()或pthread_cond_timedwait()使用的锁。被阻塞的线程应该根据调度策略(如果可用),竞争这个互斥锁,并且就像每个线程调用pthread_mutex_lock().pthread_cond_broadcast() 或pthread_cond_signal() 函数可以被一个线程调用,无论它当前是否拥有调用pthread_cond_wait()或 pthread_cond_timedwait()的线程在它们等待中相关联条件变量的锁。但,如果需要可预测的调度行为,则mutex应该被调用 pthread_cond_broadcast()或pthread_cond_signal()的线程锁定。如果当年情没有线程被阻塞在cond上,pthread_cond_broadcast()和pthread_cond_signal()应该没有作用。返回值:如果成功,pthread_cond_broadcast() 和 pthread_cond_signal()应该返回0,否则,返回一个表示错误的错误号。错误:如果以下,pthread_cond_broadcast()和pthread_cond_signal()出错:EINVAL:值cond未指向一个已经初始化的条件变量 这些函数不应该返回错误代码[EINTR]
*/
6 信号量
6.1 信号量用在多线程多任务同步的,一个线程完成某一个动作就通过信号量告诉别的线程,别的线程再进行某个操作。信号量不一定是锁定一个资源,而是流程上的概念,比如:有A和B两个线程,B线程要等A线程完成某一个任务后再进行自己下面的步骤,这个任务并不一定是锁定某一个资源,还可以进行一些计算或者数据处理之类的工作。
信号量与互斥锁和条件变量的主要不同在于“灯”的概念,灯亮则意味着资源,灯灭则意味着资源不可用。信号量作用主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。
信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。信号量的类型为sem_t,对应的头文件为<semaphore.h>:
sem_t sem;
Linux提供的信号量操作函数原型如下:
/*名称:sem_init :初始化一个未指定名称的信号量
*/#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);/*
描述: sem_init() 初始化由sem指向地址处的未指定名称的信号量。这个值参数为信号量执行初始值。pshared参数表明信号量在一个进程的线程之间共享,或者在进程之间。如果pshare值为0,则在一个进程的线程之间共享这个信号量,并且应该位于所有线程可见的某个地址(例如,全局变量,或者一个在堆上动态分配的地址)。pshared非0,则在进程之间共享,并且位于一个(见shm_open(3), mmap(2), 和 shmget(2))共享内存区域。(由于一个由fork创建的子进程继承了其父进程的内存映像,它也可以访问这个信号量)可以访问共享内存区域的任何进程可以使用sem_post(3), sem_wait(3)等操作信号量。 初始化一个已经被初始化的信号量导致未定义行为。返回值:sem_init()成功返回0,出错返回-1,而errno设置为表示错误。错误:EINVAL :值超出了SEM_VALUE_MAX.ENOSYS :pshared非0,但系统不支持进程共享的信号量*//*名称:sem_destroy : 消耗未指定名称的信号量*/
#include <semaphore.h>
int sem_destroy(sem_t *sem);/*描述: sem_destroy()销毁sem指向地址所在的信号量。仅已经由sem_init(3)初始化的信号量才使用sem_destroy()销毁。 销毁当前正在阻塞一个其它进程或线程的信号量产生未定义结果。在信号量被使用sem_init再次初始化前,使用已经被销毁的信号量产生未定义结果。返回值:sem_destroy()成功返回0,出错返回-1,而errno设置为表示错误。 错误:EINVAL sem不是有效信号量。*//*名称:sem_wait, sem_timedwait, sem_trywait -锁定信号量。
*/
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);/*对glibc的特性测试宏要求 sem_timedwait(): _POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600描述:sem_wait()减少(锁定)由sem指定的信号量。如果信号量的值大于0,则减少进行,并且函数立即返回。如果信号量当前值为0,则这个调用阻塞,直到它变得可执行官减少(即:信号量值上升大于0),或者信号处理程序中断这个调用。sem_trywait()与sem_wait()相同,除了如果不能立即执行减小,则调用返回错误(errno设为EAGAIN)而不是阻塞。sem_timedwait()与sem_wait()相同,除了abs_timeout指定一个在不能立即执行减少时应该阻塞的时长限制。abs_timeout参数指向一个结构体,其用从 Epoch, 1970-01-01 00:00:00 +0000 (UTC)以来的秒和纳米指定了一个绝对超时。这个结构体定义如下:struct timespec {time_t tv_sec; // Seconds long tv_nsec; // Nanoseconds [0 .. 999999999] };如果到调用时,超时已经耗尽,则sem_timewait()由超时错误出错(errno设为ETIMEOUT)。如果能立即执行操作,则sem_timewait(),则sem_timedwait()不会出现超时错误而失败,不论abs_timeout的值。另外,在这种情况下不检查abs_timeout的有效性。返回值:所有这些函数成功时都返回0;出错时,信号量的值不被更改,返回-1,并且errno被设置称表示错误。错误:EINTR : 调用被信号处理程序中断。EINVAL:sem不是有效信号量。
以下其它错误来自sem_trywait():EAGAIN :不能执行操作,不阻塞(例如,信号量当前值为0).
以下其它错误来自sem_timedwait():EINVA:abs_timeout.tv_nsecs 的值小于0,大于或等于1000百万。ETIMEDOUT:在锁定信号量强,调用超时。
*//*名称:sem_post -解锁信号量
*/
#include <semaphore.h>
int sem_post(sem_t *sem);/*描述: sem_post()增加(解锁)sem指向的信号量。如果信号量的值之后变得大于0,则在sem_wait调用中被阻塞的另一个进程或线程被唤醒,并且继续锁定这个信号量。返回值:sem_post()成功返回0;出错,信号量的值保持不变,返回-1,并且设置errno来表示错误。错误:EINVAL sem是一个无效的信号量 EOVERFLOW:一个信号量的最大可用值被超过。
*/
/*名称:sem_getvalue - 获取信号量的值
*/
#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);/*描述:sem_getvalue()放置sem指向的信号量的当前值到由sval指向的整数。如果一个或多个进程或线程被阻塞等待用sem_wait锁定信号量,POSIX.1-2001对在sval中返回的值有两个可能:返回0或者一个负数,其绝对值是当前阻塞在sem_wait中的进程或线程数目。Linux采取前一种行为。返回值: sem_getvalue()成功返回0,出错,返回-1,并且设置errno表示错误。错误:EINVAL sem不是一个有效的信号量。
*/