目录
一.线程互斥
1.1 相关概念
1.2 互斥量mutex
1.3 互斥量的接口
1.4 总结
1.5 互斥锁实现原理(锁的原理)
二.可重入函数和线程安全
2.1 概念
三.死锁
3.1 概念
3.2 死锁的必要条件
3.3避免死锁
四.线程同步
4.1 同步的概念
4.2 为什么需要同步
4.3 条件变量
4.4 条件变量函数
4.5 总结
并不一定所有的共享资源是临界资源,是多个线程访问的资源才是临界资源。
这时因为有4个线程,都访问了ticket这个变量,于是ticket就是临界资源,访问临界资源的代码就是临界区,于是有
原因:
当一个线程在进行判断时,另外一个线程可能正好也进行判断,此时两个线程的ticket的值相同,判断为真后,就都进入了if的语句进行减减操作。
出现负数的原因是,当ticket为1时,由于usleep,当一个线程if判断成功,进入代码,由于usleep语句,可能导致其它线程if判断为真,因为此时ticket值还是1,此时都进行减减操作。在进行减减时,有先后顺序,因为进入循环的时间可能不一样。一个减完为0,另外一个线程减为-1,最后一个线程减为-2。
如果同时进入,应是操作多次,只是导致操作了一次。
此时线程执行的函数是一个不可重入函数,因为重入后会出错。
解决上面的办法:
要做到这三点,本质需要一把锁,Linux上提供的这把锁叫互斥量。
初始化互斥量有两种方法:
方法1:静态分配
//使用宏来静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2:动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
注意:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
调用thread_mutex_lock时可能会遇到下面的情况:
修改上面有问题的代码:
线程互斥本质就是当一个线程访问临界资源,另外一个线程不能访问临界资源。
锁就是互斥量。一般怎么实现互斥,通过对临界资源加锁。当一个线程占领到锁时,其它线程只能阻塞等待线程解锁。
所有线程看到的时同一把锁,锁也是临界资源,锁得保证自生是原子性的。加锁的过程和解锁的过程都是原子性的。
如何理解POSIXpthread库中的互斥量?
OS种可能有很多线程在申请锁,所以锁需要进行管理。
伪代码:互斥量在库中一定会有两个变量
struct mutex{
......
int val;//0表示上锁,1表示没有上锁
wait_queue *head//等待队列,当锁被占用需要被放入等待队列中
}
一次保证只有一个线程进入临界区,当CPU上调度该线程的时间片到了,要切换到别的线程时,也不会有影响,别的线程还是进入不到临界区,因为还没有解锁。
所以加锁之后一般效率都会比较低,加锁之后只能一个线程运行临界区代码,并行变成串行。
锁是怎么保证自己的原子性的?
为了实现互斥锁的操作,大多数体系结构都提供了swap或者exchange指令,该指令是将寄存器和内存单元的数据想交换,由于只有一条指令,保证了原子性。
线程安全强调线程,线程执行完是否没有问题,可重入函数强调函数,函数执行完是否没有问题。
总的来说就是一个函数是可重入的,线程是安全的,一个函数不是可重入的,线程不安全。
死锁是指在一组进程中,各个线程均占有不释放的资源,但因互相申请被其它线程所占用不会释放的资源而处于一种永久等待状态。
举个例子:线程1有锁a,线程2有锁b,并且线程1,2不会释放锁。但是线程1申请锁b。线程2申请锁a。都申请不到,因为线程1和线程2都不会释放,导致线程1,2都一直阻塞等待锁的释放。
死锁的必要条件意思是,要产生死锁必须拥有的条件,缺1不可。
前三个条件是一个正常执行流就有的条件,导致死锁的主要条件就是第四个条件。
一个线程可以导致死锁: 自己申请自己的锁
当只有互斥的情况下,当一个线程访问某个变量时,再其它线程改变状态之前,这个线程什么也做不了,只能等待。
例如:一个线程往队列放数据,一个线程往队列读数据。当读数据的线程发现队列为空时,只能等待,知道另一线程往线程里写数据。
我们发现这样效率时很低下的,我们可以改变一种思路,当线程1发现队列里没数据时,会通知线程2往队列里放数据,当线程2发现队列里的数据满的时候,会通知线程1从队列里读数据。
同步:再保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题。
与互斥相比较,互斥是保证线程的安全。但是会导致饥饿问题,同步有效解决了饥饿问题,使多线程协同高效完成某些事情。
条件变量可以理解成,保存条件的变量。线程可以通过函数来发送或者识别条件变量。来使线程根据条件变量进行不同的动作。
条件变量的初始化也有两种方法:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
PTHREAD_COND_INITIALIZER是一个宏。
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
作用:销毁条件变量
参数:cond:要销毁的条件变量
返回值:成功返回0,失败返回1
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
为什么pthread_cond_wait需要互斥量作为参数?
首先pthread_cond_wait肯定使因为条件不满足才会进行等待。
要判断条件一定会在临界区里,临界区因为互斥使上锁的。
等待的时候,需要将锁释放释放,不然别的线程进不了临界区,改变不了条件,就会导致当前线程一直等,导致死锁。
等待完毕被唤醒时还需要将锁锁上。
结论:在调用pthread_cond_wait时,需要别的线程进到临界区来修改条件,会自动释放锁。
当条件满足被唤醒时,该函数会让该线程重新占有锁。
int pthread_cond_signal(pthread_cond_t *cond);
怎么理解条件变量?
OS中线程中可能有很多的条件变量,OS需要对其进行管理。先描述后组织。
根据线程要通过条件变量,一个线程再条件变量上等待,一个将通知信息发送到条件变量上。大概有
struct cond{
......
int val;//条件是否满足
wait_queue *head;//条件不满足需要等待。
......
}
简单案例使用上面的函数:
一个线程等待条件变量的满足,一个线程发通知给条件变量,使其满足条件。
1 #include
2 #include
3 #include
4 //等待条件成立需要互斥量
5 pthread_mutex_t mutex;
6 pthread_cond_t cond;
7 //等待条件变量成立
8 void *rountion1(void *arg){
9 char *str=(char *)arg;
10 while(1){
11 pthread_cond_wait(&cond,&mutex);
12 printf("%s:活动......\n",str);
13 }
14
15 }
16 //唤醒条件变量
17 void *rountion2(void *arg){
18 char *str=(char *)arg;
19 while(1){
20 sleep(2);
21 pthread_cond_signal(&cond);
22 printf("%s meet the condtion\n",str);
23 }
24
25 }
26
27 int main(){
28 pthread_mutex_init(&mutex,NULL);
29 pthread_cond_init(&cond,NULL);
30 pthread_t t1,t2;
31 pthread_create(&t1, NULL, rountion1, (void *)"thread 1");
32 pthread_create(&t2, NULL, rountion2, (void *)"thread 2");
33
34 pthread_join(t1, NULL);
35 pthread_join(t2, NULL);
36 pthread_cond_destroy(&cond);
37 pthread_mutex_destroy(&mutex);
38 return 0;
39 }
同步就是在保证数据安全的前提下,让线程按照某种特定的顺序来访问临界资源。
同步与互斥的关系:互斥是保证数据的安全,同步在互斥的前提下,来提高线程之间的效率。