飞道的博客

Linux___线程互斥与同步

571人阅读  评论(0)

1. 线程互斥

  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。

1.1 临界资源、临界区、原子性

  • 临界资源被多个执行流同时访问的共享资源就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么别做,要么做完。

1.2互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。

下面写个多个线程操作共享变量来抢票的售票系统代码:


1.为什么可能无法获得争取结果?

  1. if 语句判断条件为真以后,代码可以并发的切换到其他线程。
  2. usleep 这个模拟漫长业务的过程中,可能有很多个线程会进入该代码段。
  3. ticket-- 操作本身就不是一个原子操作。


要解决以上问题,需要做到三点:

  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时要求执行临界区的代码并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量(mutex)。

1.3互斥量的接口

初始化互斥量:

销毁互斥量:

销毁互斥量需要注意

  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

互斥量加锁和解锁:


注意:在特定线程/进程拥有锁的期间,有新的线程来申请锁,pthread_ mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。unlock之后对线程进程唤醒操作。此外,加锁的粒度越小越好

  • 锁的申请是将lock由1变为0
  • 锁的销毁时将lock由0变为1

改进上面的抢票系统:

1.4互斥量(锁)实现原理


每个线程的寄存器是私有的,在修改数据的时候用的不是拷贝而是xchgb交换,将寄存器的值和内存的值互换。这保证了锁的原子性。因为其他线程申请的话,内存的值为0,申请不到锁。lock:0表示被占, 1表示可以被申请

  1. 整个过程为1的mutex只有一份。
  2. exchange一条汇编完成了寄存器和内存数据的交换。

2. 可重入函数&&线程安全

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

2.1 常见的线程不安全的情况

  1. 不保护共享变量的函数
  2. 函数状态随着被调用,状态发生变化的函数
  3. 返回指向静态变量指针的函数
  4. 调用线程不安全函数的函数

3. 死锁

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

3.1 死锁四个必要条件

  1. 互斥条件:一个资源每次只能被一个执行流使用。
  2. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

3.2 避免死锁的方法

  1. 破坏死锁的四个必要条件
  2. 加锁顺序一致
  3. 避免锁未释放的
  4. 资源一次性分配

4.线程同步

同步概念:在保证数据安全(一般使用加锁方式)的情况下,让线程能够按照某种特定的顺序访问临界资源,就叫做同步

  • 为什么要存在同步
    • 使多线程同步高效的完成某些事情。

同步实现的事情:当有资源的时候,可以直接获取资源,没有资源的时候,线程进行等待,等待另外的线程生产一个资源,当生产完成的时候,通知等待的线程。

4.1条件变量

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中,这种情况就需要用到条件变量。

竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。

条件变量的本质:PCB等待队列+两个接口(等待接口+唤醒接口)

4.2条件变量函数

  1. 定义条件变量
pthread_cond_t  条件变量类型
  1. 初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
	cond:传入条件变量的地址
	attr:条件变量的属性,一般设置为NULL,采用默认属性		
  1. 销毁(释放动态初始化的条件变量所占用的内存)
int pthread_cond_destroy(pthread_cond_t *cond)
  1. 等待条件满足(将调用该等待接口的执行流放入PCB等待队列当中)
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
	cond:传入条件变量的地址
	restrict mutex:传入互斥锁变量的地址
  1. 唤醒等待(通知PCB等待当中的执行流来访问临界资源)
int pthread_cond_signal(pthread_cond_t *cond);
参数:
	cond:传入条件变量的地址
	//唤醒至少一个PCB等待队列当中的线程

4.3 为什么会有互斥锁?

  • 同步并没有保证互斥,意味着不同的执行流可以在同一时刻去访问临界资源,所以需要条件变量中的互斥锁来保证互斥,各执行流在访问临界资源的时候,只有一个执行流可以访问。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t lock;
pthread_cond_t cond;

void *task_t2(void *arg)
{
   
  const char *name = (char*)arg;
  while(1){
   
    pthread_cond_wait(&cond, &lock);
    printf("get cond : %s 活动...\n", name);
  }
}

void *task_t1(void *arg)
{
   
  const char *name = (char*)arg;
  while(1){
   
    sleep(rand()%3+1);
    pthread_cond_signal(&cond);
    printf("%s signal done...\n", name);
  }
}


int main()
{
   
  pthread_mutex_init(&lock, NULL);
  pthread_cond_init(&cond, NULL);

  pthread_t t1,t2,t3,t4,t5;
  pthread_create(&t1, NULL, task_t1, "thread1");
  pthread_create(&t2, NULL, task_t2, "thread2");
  pthread_create(&t3, NULL, task_t2, "thread3");
  pthread_create(&t4, NULL, task_t2, "thread4");
  pthread_create(&t5, NULL, task_t2, "thread5");

  pthread_join(t1, NULL);
  pthread_join(t2, NULL);
  pthread_join(t3, NULL);
  pthread_join(t4, NULL);
  pthread_join(t5, NULL);

  pthread_mutex_destroy(&lock);
  pthread_cond_destroy(&cond);
  return 0;
}


转载:https://blog.csdn.net/weixin_50843868/article/details/115874565
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场