文章目录
1.线程互斥背景知识(临界资源,互斥,原子性定义)
- 临界资源:多执行流下共享的资源称为临界资源,每个线程内部访问临界资源的代码称为临界区
- 互斥:为了保护临界资源,在任何时刻只有一个执行流进入临界区访问临界资源。
- 原子性:不会被任何调度打断的操作。通常有两态1.完成 2.未完成
2.对临界资源保护的重要性
如果临界资源不保护
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int cout=1000;
void*Del(void*meg)
{
int Num=(int)meg;
while(1)
{
if(cout>0)
{
usleep(10000);
cout--;
printf("pthread%d cout=%d\n",Num,cout);
}
else
{
break;
}
}
}
int main()
{
pthread_t tid[4];
for(int i=0;i<4;i++)
{
pthread_create(&tid[i],NULL,Del,(void*)i);
}
for(int i=0;i<4;i++)
{
pthread_join(tid[i],NULL);
}
return 0;
}
此时会出现将cout减为负数的情况,这显然是不正确的
原子性的解释
造成上述原因是cout- -这个操作不是原子性的 cout- -需要经过下面的步骤
1.将内存中的cout值读取到cpu上
2.对cout值进行-1.
3.将cout的值写回内存中
设cout值为100
当线程1刚把内存中的cout读到cpu中,此时搞好进行线程切换,这个cout值会作为线程上下文信息被保留下来。线程1认为此时cout值为100
线程2切换回后,当线程2执行时间比较长时,线程2把cout读到cpu中,cout- -执行完后,又将cout写回到内存上,此时cout值变为99。
线程切换回线程1时,线程1的上下文信息加载到cpu中,但这时线程1认为cout为100。所以就会出现两个线程操作的cout的值不一致的错误
3.临界资源的保护(Linux互斥锁pthread_mutex_t)
根据上面的分析,我们知道:
线程与线程之间必须有互斥的约束。一个线程在访问临界资源,此时其他线程不能再访问临界资源,在Linux中这种保护通过互斥锁来实现。当一个线程申请到锁,其他线程就会阻塞等待锁的释放,保护了临界资源
初始化互斥锁(pthread_mutex_init(pthread.h))
如上图给出了两种初始化pthread_mutex_t的方法,一种是pthread_mutex_init函数初始化
另一种是 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
参数解释:
mutex:要初始化的互斥锁
attr:互斥锁的属性,一般为NULL默认
返回值:成功返回0,失败返回错误码
互斥锁的销毁(pthread_mutex_destroy(pthread.h))
参数解释:只需要传入要销毁的互斥锁即可
返回值:成功返回0,失败返回错误码。
临界资源加锁与解锁(pthread_mutex_lock/pthread_mutex_unlock)
注意:加锁有损于性能。
eg:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int cout=100000;
pthread_mutex_t lock;
void*Del(void*meg)
{
int Num=(int)meg;
while(1)
{
pthread_mutex_lock(&lock);//进入临界资源时加锁
if(cout>0)
{
usleep(1000);
cout--;
printf("pthread%d cout=%d\n",Num,cout);
pthread_mutex_unlock(&lock);//出临界资源解锁
}
else
{
//如果此时不需要--,此时也要释放锁
pthread_mutex_unlock(&lock);
break;
}
}
}
int main()
{
pthread_t tid[4];
pthread_mutex_init(&lock,NULL);
for(int i=0;i<4;i++)
{
pthread_create(&tid[i],NULL,Del,(void*)i);
}
for(int i=0;i<4;i++)
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&lock);
return 0;
}
如图:这样cout就不存在负数的情况了。但这样还存在问题,因为线程刚刚将锁释放,这个线程对于锁的竞争比其他线程更强,所以可能存在一个线程使得cout减少到0,这时需要引入同步机制,让每个线程都有机会使得cout-- 。
注意:
1.线程申请到锁后在临界区也可以进行线程切换,即便当前线程被切换其他线程也无法进入临界区
4.锁的原子性分析
如上代码,每个线程的状态只有两种。
1.已经申请到锁 2.已经将锁释放
锁可以多个线程所看到,所以锁首先是临界资源。
锁也需要被保护,所以申请锁的过程就是原子性的
加锁与释放锁的原子性的实现
大多数体系结构提供了exchange或swap指令,用来交换寄存器和内存数据。因为只有一条指令,所以这个过程是原子性的。
加锁与解锁流程图:
5.可重入与线程安全
可重入函数:如果当在一个执行流下在执行这个函数,另一个执行流也执行这个函数。如果运行不会有问题,这时称为这个函数为可重入函数。
线程安全:当在多线程并发执行代码时不会出现不同的结果(常见为全局变量和静态变量等),称为线程安全
函数可重入则一定是线程安全的,线程安全不一定是可重入的。
一个讨论的是函数,一个讨论的是线程
6.死锁
死锁:线程因为编码失误导致申请的锁没有释放而导致永久等待的情况。
eg:连续申请两次锁。
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int cout=1000;
pthread_mutex_t lock;
void*Del(void*meg)
{
int Num=(int)meg;
while(1)
{
pthread_mutex_lock(&lock);//进入临界资源时加锁
pthread_mutex_lock(&lock);//连续申请两次锁导致死锁
if(cout>0)
{
usleep(1000);
cout--;
printf("pthread%d cout=%d\n",Num,cout);
pthread_mutex_unlock(&lock);//出临界资源解锁
}
else
{
//如果此时不需要--,此时也要释放锁
pthread_mutex_unlock(&lock);
break;
}
}
}
int main()
{
pthread_t tid[4];
pthread_mutex_init(&lock,NULL);
for(int i=0;i<4;i++)
{
pthread_create(&tid[i],NULL,Del,(void*)i);
}
for(int i=0;i<4;i++)
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&lock);
return 0;
}
阻塞挂起(资源等待队列)
死锁的必要条件
- 互斥条件:线程A有线程B的锁,线程B有线程A的锁。
- 请求与保持条件:A线程拥有锁但还申请锁
- 不剥夺条件:A线程拥有锁,A线程不会被剥夺
- 循环等待条件:线程A有线程B的锁,线程B有线程A的锁。线程A申请A锁,线程B申请B锁导致死锁。
解除死锁只要破坏死锁的4个必要条件即可