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个必要条件即可

Logo

更多推荐