线程的同步与互斥

线程的互斥

由于一个进程中的多个线程共享同一个虚拟地址空间。除了一些私有数据,其余大部分资源都是共享的,为了保证线程安全一次只允许一个线程对其访问,这些数据就称为临界资源,包含临界资源的代码段称为临界区。我们可以通过一把锁实现:当线程进入临界区执行时,不允许其他线程进入该临界区。如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区,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的实现分为三个操作:

  1. pthread_mutex_unlock(&mutex);
  2. pthread_cond_wait(&cond);
  3. pthread_mutex_lock(&mutex);
    若前两个操作中间可以被打断,那么就有可能出现,A线程加锁后断判资源不足,执行完第一步解锁,然后还没来得及执行第二步等待,另一个线程B就抢到锁后加锁补充资源,解锁发送信号,进入等待状态。此时由于A还没执行第二部进入等待状态所以无法收到信号,所以A执行第二步等待的时候B也在等待,这样就造成了死锁。
    所以pthread_cond_wait设置了两个参数来保证前两步的原子操作

代码演示

使用COUNT个执行流打开开关,另外再使用COUNT个执行流关闭开关,开关不能连续打开或者关闭两次。为了防止唤醒同一个等待队列中的角色,所以多个角色要使用多个条件变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <pthread.h>

#define COUNT 4

int status = 0;
pthread_mutex_t mutex;
// 不同角色在不同条件变量队列上等待,防止误唤醒阻塞
pthread_cond_t switch_on;
pthread_cond_t switch_off;

void* thread_on(void* arg) {
while (1) {
pthread_mutex_lock(&mutex);
// 循环判断防止当前不符合条件被唤醒后直接操作临界资源
while (status == 1) {
pthread_cond_wait(&switch_on, &mutex);
}
status = 1;
printf("Turn on the switch\n");
// 先解锁可以减少锁冲突的概率,提高性能
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&switch_off);
}
return NULL;
}

void* thread_off(void* arg) {
while (1) {
pthread_mutex_lock(&mutex);
while (status == 0) {
pthread_cond_wait(&switch_off, &mutex);
}
status = 0;
printf("Turn off the switch\n");
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&switch_on);
}
return NULL;
}

int main() {
pthread_t tid_on[COUNT], tid_off[COUNT];
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&switch_on, NULL);
pthread_cond_init(&switch_off, NULL);
for (int i = 0; i < COUNT; i++) {
if (pthread_create(&tid_on[i], NULL, thread_on, NULL) != 0) {
printf("create thread_on error\n");
return -1;
}
}
for (int i = 0; i < COUNT; i++) {
if (pthread_create(&tid_off[i], NULL, thread_off, NULL) != 0) {
printf("create thread_off error\n");
return -1;
}
}
for (int i = 0; i < COUNT; i++) {
pthread_join(tid_on[i], NULL);
pthread_join(tid_off[i], NULL);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&switch_on);
pthread_cond_destroy(&switch_off);
return 0;
}

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,并设置错误码