线程的互斥
由于一个进程中的多个线程共享同一个虚拟地址空间。除了一些私有数据,其余大部分资源都是共享的,为了保证线程安全一次只允许一个线程对其访问,这些数据就称为临界资源,包含临界资源的代码段称为临界区。我们可以通过一把锁实现:当线程进入临界区执行时,不允许其他线程进入该临界区。如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区,Linux上提供的这把锁叫互斥量
互斥量
初始化/销毁互斥量
静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
动态初始化int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
mutex: 输出型参数,要初始化的互斥量
attr: 互斥锁属性一般设置NULL
成功返回0,失败返回错误码
销毁int pthread_mutex_destroy(pthread_mutex_t *mutex);
静态初始化的互斥量不需要销毁;不要销毁一个已经加锁的互斥量;已经销毁的互斥量,要确保后面不会有线程再尝试加锁
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
trylock非阻塞加锁int pthread_mutex_unlock(pthread_mutex_t *mutex);
成功返回0,失败返回错误码
互斥锁原理
互斥锁本质是一个计数器,由于i或者i都不是原子操作,要保证互斥锁的操作为原子操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,只有一条指令,首先将0放入寄存器中,然后执行执行swap和内存单元的数据交换,然后判断这个数据是否为1,若为1,则此操作就是加锁操作。加锁后访问临界资源,此时寄存器中的数据为1,访问完成后,再次将数据交换回来,此时内存单元的数据就变为1,这一步叫解锁操作,加锁和解锁都是一步完成的,保证了原子性
线程的同步
使用互斥锁可以解决线程安全的问题,保证多线程下临界资源数据的安全性,但是仅仅互斥还是会存在一些问题,某个线程获取锁之后,发现数据没有就绪,又立刻释放锁,如果这个线程的优先级很高,那么就可能在释放了锁之后又立刻尝试获取锁,再立刻释放,依次类推,这样虽然并没有发生死锁,但是这个线程空转又占用了锁资源,导致其他线程很难获取到这个锁
linux可以通过条件变量和信号量两种方法来实现线程的同步
条件变量
当一个线程互斥的访问某个变量时,若不满足某一条件,则挂起等待,知道条件满足被唤醒
初始化/销毁条件变量
静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
动态初始化int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
cond: 输出型参数,要初始化的条件变量
attr: 条件变量属性一般设置NULL
销毁int pthread_cond_destroy(pthread_cond_t *cond)
成功返回0,失败返回错误码
等待/唤醒条件变量
等待
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
timedwait限时的阻塞等待
唤醒所有等待进程int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒至少一个线程int pthread_cond_signal(pthread_cond_t *cond);
成功返回0,失败返回错误码
条件变量是为了解决某些情况下互斥锁低效的问题,因此对条件变量的操作,必然要和互斥锁密切相关,pthread_cond_wait的实现分为三个操作:
- pthread_mutex_unlock(&mutex);
- pthread_cond_wait(&cond);
- pthread_mutex_lock(&mutex);
若前两个操作中间可以被打断,那么就有可能出现,A线程加锁后断判资源不足,执行完第一步解锁,然后还没来得及执行第二步等待,另一个线程B就抢到锁后加锁补充资源,解锁发送信号,进入等待状态。此时由于A还没执行第二部进入等待状态所以无法收到信号,所以A执行第二步等待的时候B也在等待,这样就造成了死锁。
所以pthread_cond_wait设置了两个参数来保证前两步的原子操作
代码演示
使用COUNT个执行流打开开关,另外再使用COUNT个执行流关闭开关,开关不能连续打开或者关闭两次。为了防止唤醒同一个等待队列中的角色,所以多个角色要使用多个条件变量
1 |
|
POSIX信号量
POSIX信号量和SystemV信号量作用相同,SystemV为内核中的计数器主要用于进程间,POSIX进程/线程间都可以,信号量本质就是一个计数器和一个pcb等待队列,可通过自身的计数器对资源计数并判断资源是否符合访问条件,符合则可以访问,不符合则阻塞等待,其他进程/线程促使条件满足后,可以唤醒pcb等待队列上的pcb,从而实现同步;也可通过使资源计数器不大于1,保证同一时间只有一个进程/线程可以访问临界资源实现互斥
初始化/销毁信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
sem: 信号量变量
pshared: 0表示线程间共享,非零表示进程间共享,如果用于线程间这个计数器是一个全局变量,如果用于进程间则在申请的共享内存中实现pcb等待队列和计数器
value: 信号量初始值
成功返回0,失败返回-1,并设置错误码
等待/发布信号量
int sem_wait(sem_t *sem);
等待信号量,会将信号量的值减1 trywait非阻塞等待 timedwait限时阻塞int sem_post(sem_t *sem);
发布信号量,将信号量的值加1,并唤醒等待进程/线程
成功返回0,失败返回-1,并设置错误码