Linux多线程(2)

线程的知识点太多,太重要,所以分成三部分进行总结学习

线程安全

多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

多个线程对临界资源进行竞争操作时若不会造成数据二义性时则线程安全;否则,此时就是不安全的

如何实现线程安全

常见的线程安全的情况
  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性
常见的线程不安全的情况
  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

在网上调研过程中看到一个总结:减少对临界资源的依赖,尽量避免访问全局变量,静态变量或其它共享资源,如果必须要使用共享资源,所有使用到的地方必须要进行互斥锁 (Mutex) 保护

所以当对临界资源使用时,尽量在必须的地方使用锁的保护

对临界资源又有两种访问,分别是同步访问和互斥访问

同步:临界资源的合理访问

异步:临界资源同一时间的唯一访问

互斥锁

互斥锁的操作就是1/0的操作

一个0或者1的计数器。1可以表示加锁,加锁就是计数-1;操作完毕之后要解锁,解锁就是计数+1;

0表示不可以加锁,不能加锁则等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//互斥锁的接口
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//函数应销毁mutex引用的mutex对象
//注意!!!
//销毁已解锁的已初始化互斥体应是安全的。试图销毁锁定的互斥体会导致未定义的行为。

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
//mutex:互斥锁变量
//attr:属性,通常为NULL
//应使用attr指定的属性初始化mutex引用的mutex

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//宏PTHREAD_MUTEX_INITIALIZER来静态的初始化锁
//互斥锁变量不一定非要全局变量--只要保证要互斥的线程都能访问到就行

int pthread_mutex_lock(pthread_mutex_t *mutex);
//锁定mutex引用的mutex对象。如果互斥体已被锁定,则调用线程应阻塞,直到互斥体可用。此操作将返回互斥对象引用的互斥对象处于锁定状态,调用线程作为其所有者。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//函数应等同于pthread_mutex_lock(),但如果mutex引用的mutex对象当前被锁定(由任何线程,包括当前线程),则调用应立即返回。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//函数应释放mutex引用的mutex对象。互斥体的释放方式取决于互斥体的type属性。如果在调用pthread_mutex_unlock()时,mutex引用的mutex对象上有线程被阻塞,导致mutex可用,调度策略应确定哪个线程应获取mutex。

互斥锁的操作步骤

  1. 定义互斥锁变量
  2. 初始化互斥锁变量
  3. 加锁
  4. 解锁
  5. 销毁互斥锁

通过一个互斥锁Demo来感受一下锁的使用

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
//模拟黄牛抢票,100张票,共有四个黄牛在抢票
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

int ticket = 100;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//定义初始化锁

void* thr_start(void* arg){
while(1){
pthread_mutex_lock(&mutex);
if(ticket > 0){
usleep(1000);
printf("yellow bull : %d----get ticket : %d\n",(int)arg,ticket);
ticket--;
}else{
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}

int main(int argc, char* argv[]){
pthread_t tid[4];
int i = 0,ret;

pthread_mutex_init(&mutex,NULL);
for(;i < 4; ++i){
ret = pthread_create(&tid[i],NULL,thr_start,(void*)i);
if(ret != 0){
printf("yellow bull no exit!");
return -1;
}
}
for(i = 0;i < 4;++i){
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mutex);
return 0;
}

线程1

这种情况黄牛抢票是比较容易的,一般只有一个黄牛能全抢到票。

但是如果把锁去掉

线程2

这样抢票就很混乱,因为没有了保护。所以锁的使用是在共享资源对它进行保护,换句话说加锁是为了保护资源,所以在这个代码中就将抢票的操作进行加锁保护。这样就只有一个黄牛可以抢到票。

死锁

在进行加锁的过程中很有可能发生死锁的情况下。

在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他的进程所占用不会释放的资源而处于一种永久等待的状态

死锁的四个条件(重点)

1、互斥条件:一个资源一次只能被一个执行流使用

我操作的时候别人不能操作

2、请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不变

拿着手里的,但是请求其他的,其他的请求不到,手里拿着的也不放开

3、不可剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺

我的锁,别人不能释放

4、循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

指在发生死锁时,必然存在一个进程资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,Pn正在等待已被P0占用的资源

死锁的产生与处理

当加锁或者解锁顺序不同时会发生死锁的情况;对锁资源的竞争以及进程/线程的加锁的推进顺序b不当

当以上四种条件被破坏时,可以预防死锁的产生

避免死锁的方法可以通过:死锁检测算法,银行家算法(推荐王道视频学习)

同步的实现

条件变量是线程同步的一种手段,条件变量用来自动阻塞一个线程,直到条件满足被触发为止。通常情况下条件变量和互斥锁同时使用

条件变量使我们可以睡眠等待某种条件出现。条件变量利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

1、一个/多个线程等待“条件变量的条件成立”而挂起;线程1如果操作条件满足,则操作,否则进行等待。

2、另一个线程使“条件成立”信号;线程2促使条件满足,唤醒等待的线程。

如果没有资源则等待(死等),生产资源后唤醒等待。

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
//条件变量的接口
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
//条件变量初始化,一般attr默认为NULL
//使用attr引用的属性初始化cond引用的条件变量。如果attr为空,则使用默认条件变量属性;效果与传递默认条件变量属性对象的地址相同。初始化成功后,条件变量的状态将被初始化。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//静态初始化条件变量

int pthread_cond_destroy(pthread_cond_t *cond);
//销毁由cond指定的给定的条件变量
//销毁当前未阻塞线程的初始化条件变量是安全的。
//试图销毁当前阻止其他线程的条件变量会导致未定义的行为。

int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
//abstime:限时等待时长,限时等待时长,超时后则返回

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
//解锁后的挂起操作(原子操作),有可能还没来得及挂起就已经有人唤醒--白唤醒--导致了死锁

int pthread_cond_signal(pthread_cond_t *cond);
//唤醒至少一个等待的

int pthread_cond_boardcast(pthread_cond_t *cond);
//广播唤醒,唤醒所有等待的人

条件变量的步骤:

1、定义条件变量

2、初始化条件变量

3、等待\唤醒定义的条件变量

4、销毁条件变量

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
68
69
70
71
72
73
74
75
76
77
78
//模拟一个skr与cxk使用比赛舞台的Demo                                  
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>

int have_stage = 1;

pthread_cond_t skr;
pthread_cond_t cxk;
pthread_mutex_t mutex;

void* thr_skr(void* arg){//skr此时要上台dancing
while(1){
pthread_mutex_lock(&mutex);
//若此时舞台有人用,那么skr进行等待
while(have_stage == 1){
pthread_cond_wait(&skr,&mutex);
}
//舞台被人使用了,此时0;因为之前1,代表可以使用
printf("skr~~ is freestyle!!!\n");
sleep(1);
//跳完舞后舞台空了出来
have_stage += 1;
//有舞台了,叫cxk来使用
pthread_cond_signal(&cxk);
pthread_mutex_unlock(&mutex);
}
return NULL;
}

void* thr_cxk(void* arg){
while(1){
pthread_mutex_lock(&mutex);
//没有舞台,那么在这里等待
while(have_stage == 0){
pthread_cond_wait(&cxk,&mutex);
}
//有了舞台就是可以唱跳rap篮球了。。
printf("cxk~~ is singing,dancing,playing rapping and basketball!!\n");
sleep(1);
have_stage -= 1;
//跳完还想跳。。因此叫skr快跳完换他跳。。
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&skr);//唤醒skr,
}
return NULL;
} int main(int argc,char * argv[]){
pthread_t tid1,tid2;
int ret;

pthread_cond_init(&skr,NULL);
pthread_cond_init(&cxk,NULL);
pthread_mutex_init(&mutex,NULL);
int i = 0;
for(i = 0;i < 2;i++){
ret = pthread_create(&tid1,NULL,thr_skr,NULL);
if(ret != 0){
printf("skr error");
return -1;
}
}
for(i = 0;i < 2; i++){
ret = pthread_create(&tid2,NULL,thr_cxk,NULL);
if(ret != 0){
printf("cxk error");
return -1;
}
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);

pthread_cond_destroy(&skr);
pthread_cond_destroy(&cxk);
pthread_mutex_destroy(&mutex);
return 0;
}

线程3

运行结果可以看到cxk和skr交替互斥的表演节目。。。

那么互斥量(mutex)保护的是什么?其实保护的是变量条件(have_stage),当互斥量被成功lock后我们就可以放心的去读取变量条件,这样就不用在担心在这期间变量条件会被其他线程修改。如果变量条件不满足条件,当前线程阻塞,等待其他线程释放条件成立信号,并释放已经lock的mutex。这样一来其他线程就有了修改变量条件的机会。当其他线程释放条件成立信号后,pthread_cond_wait函数返回,并再次lock

pthread_cond_wait的工作流程可以总结为:unlock mutex,start waiting -> lock mutex。

while的作用

在变量条件处为什么不用if做判断而是用while,这是因为pthread_cond_wait的返回不一定意味着其他线程释放了条件成立信号。也可能意外返回。这种被称为假唤醒,在Linux中带阻塞功能的system call都会在进程中收到了一个signal后返回。这就是为什么使用while来检查的原因。因为不能保证wait函数返回的一定就是条件满足,如果条件不满足,那么我们还需要继续等待

signal条件变量的考虑

解锁互斥量mutex和发出唤醒信号是两个单独的操作,所以就存在一个顺序的问题

(1) 按照 unlock(mutex); condition_signal()顺序,当等待线程被唤醒时,因为mutex已经解锁,因此被唤醒的线程(skr)很容易就锁住了mutex然后从conditon_wait()中返回了。

(2) 按照 condition_signal(); unlock(mutex)顺序,当等待线程被唤醒时,它试图锁住mutex,但是如果此时mutex还未解锁,则线程又进入睡眠,mutex成功解锁后,此线程在再次被唤醒并锁住mutex,从而从condition_wait()中返回。

-------------The End-------------