【Linux】:线程(二)互斥

news/2024/7/5 2:31:26

互斥与同步

  • 一.线程的局部存储
  • 二.线程的分离
  • 三.互斥
    • 1.一些概念
    • 2.上锁
    • 3.锁的原理
    • 4.死锁

一.线程的局部存储

例子

在这里插入图片描述

在这里插入图片描述

可以看到全局变量是所有线程共享的,如果我们想要每个线程都单独访问g_val怎么办呢?其实我们可以在它前面加上__thread修饰。

在这里插入图片描述

在这里插入图片描述

这就相当于把g_val从全局变量去储存到了局部储存里。每个线程可以单独访问自己的g_val。(注意__thread只能定义内置类型)

在这里插入图片描述

二.线程的分离

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

如果不关心线程的返回值,join是一种负担(因为它会阻塞我们的主线程),这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

在这里插入图片描述

在这里插入图片描述

这个函数在主函数和当前线程里都可以使用。

三.互斥

1.一些概念

临界资源:多线程执行流共享的资源就叫做临界资源 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

多个线程并发的操作共享变量,会带来一些问题。

例如,一个抢票系统

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int ticket=100;
void *route(void *arg)
{
  char *id = (char*)arg;
  while(1) 
  {
    if (ticket>0) 
    {
      usleep(1000);
      printf("%s sells ticket:%d\n", id, ticket);
      ticket--;
    } 
    else 
    {
      break;
    }
  }
} 
int main()
{
  pthread_t t1, t2, t3, t4;
  pthread_create(&t1, NULL, route, (void*)"thread 1");
  pthread_create(&t2, NULL, route, (void*)"thread 2");
  pthread_create(&t3, NULL, route, (void*)"thread 3");
  pthread_create(&t4, NULL, route, (void*)"thread 4");
  pthread_join(t1, NULL);
  pthread_join(t2, NULL);
  pthread_join(t3, NULL);
  pthread_join(t4, NULL);

  return 0;
}

在这里插入图片描述

可以看到已经抢到了负数,很明显是不符合实际的。这是因为在执行打印ticket操作时,操作系统需要从CPU里读取ticket数据,而当一个线程已经打印了ticket=0后,再执行了减减操作,ticket变为了-1,将ticket的值再CPU里更新;这时切换到了另一个线程,而该线程又恰好正要执行打印ticket操作,那么它从CPU里读取了数据,打印出来就为了负数。

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

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

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

在这里插入图片描述

2.上锁

创建锁

在这里插入图片描述

调用函数时可能会出现以下情况:

1.互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
2.发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

加锁

在这里插入图片描述

在这里插入图片描述

修改代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>

pthread_mutex_t mutex=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;//初始化
int ticket=100;
void *route(void *arg)
{
  char *id =(char*)arg;
  while(1) 
  {
    pthread_mutex_lock(&mutex);//上锁
    if(ticket>0) 
    {
      usleep(1000);
      printf("%s sells ticket:%d\n", id, ticket);
      ticket--;
      pthread_mutex_unlock(&mutex);//解锁
    } 
    else 
    {
      pthread_mutex_unlock(&mutex);//解锁
      break;
    }
  }
} 
int main()
{
  pthread_t t1, t2, t3, t4;
  pthread_create(&t1, NULL, route, (void*)"thread 1");
  pthread_create(&t2, NULL, route, (void*)"thread 2");
  pthread_create(&t3, NULL, route, (void*)"thread 3");
  pthread_create(&t4, NULL, route, (void*)"thread 4");
  pthread_join(t1, NULL);
  pthread_join(t2, NULL);
  pthread_join(t3, NULL);
  pthread_join(t4, NULL);

  pthread_mutex_destroy(&mutex);//销毁

  return 0;
}

在这里插入图片描述

我们发现票数问题得到了解决,但是票全被一个线程抢走了,这是怎么回事呢?其实是由于不同线程对于锁的竞争能力是不同的,这里当线程2释放锁后,马上又去申请了锁,导致锁一直被线程2拿着,出现了线程饥饿问题。我们可以在外面加上sleep函数,让每个线程释放锁后休息一段时间,避免锁一直在某一个线程上。

在这里插入图片描述

在这里插入图片描述

3.锁的原理

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题。

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下。

在这里插入图片描述

movb语句是把al寄存器置零。

xchgb语句就是把al寄存器里的数据交换与内存里的mutex(1)变量进行一次交换(此时mutex就变为了0)。注意mutex是所有线程共享,也就是说其实1只有一份,当第一个进程将mutex里的1交换走后,后面的线程就无法拿到1,也就是上锁了。

4.死锁

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

死锁的必要条件

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

避免死锁

破坏死锁的四个必要条件。
加锁顺序一致。
避免锁未释放的场景。
资源一次性分配。

http://lihuaxi.xjx100.cn/news/1888253.html

相关文章

Vue学习计划-Vue2--VueCLi(四)组件传值和自定义事件

1. 组件传值 组件化编码流程&#xff1a; 拆分静态组件&#xff1a;组件要按照功能点拆分&#xff0c;命名不要与html元素冲突实现动态组件&#xff1a;考虑好数据的存放位置&#xff0c;数据是一个组件在用&#xff0c;还是一些组件在用&#xff1a; 一个组件在用&#xff0c…

LeeCode刷题

寻找数组的中心下标 给定一个整数nums&#xff0c;请编写一个能够返回数组“中心下标”的方法 中心下标是数组的一个下标&#xff0c;其左侧所有元素相加的和等于右侧所有元素相加的和。如果数组不存在中心下标&#xff0c;返回-1.如果数组右多个中心下标&#xff0c;应该返回…

飞天使-docker知识点1-安装docker以及手动制作镜像

文章目录 docker 的好处安装dockerdocker imagesimages 导出与导出删除镜像,指定端口启用容器启停批量关闭正在运行的容器 docker 的镜像制作之下载并安装好nginxdocker 的镜像制作之提交镜像 docker 的好处 快速部署&#xff1a;短时间内可以部署成百上千个应用&#xff0c;更…

K8S(一)—安装部署

目录 安装部署前提以下的操作指导(在master)之前都是三台机器都需要执行 安装docker服务下面的操作仅在k8smaster执行 安装部署 前提 以下的操作指导(在master)之前都是三台机器都需要执行 关闭防火墙 [rootk8smaster ~]# vim /etc/selinux/config [rootk8smaster ~]# swa…

Linux内核参数配置说明

Linux内核参数配置说明 一、引言 Linux内核参数是操作系统运行过程中的重要配置&#xff0c;它们决定了系统资源的管理方式、性能优化以及系统安全等方面的特性。本文将详细介绍Linux内核参数的配置说明&#xff0c;并给出CentOS 7推荐的内核参数设置。 二、Linux内核参数概…

Temporal 常见 FQ 速查

1、启动 worker 失败 INFO No Lofigured for temporal client. Created default one. Unable to create client failed reaching server: upstream connect error or disconnect/reset before headers. reset reason: connection failure 解决&#xff1a;没找到链接资源&…

java商城项目的实现更新

先把需要修改的用户按id查询 1.接收参数&#xff0c;collection就是HashMap的values&#xff0c;这个values中只有id。因为查询需要把查询出的数据返回&#xff0c;这里对类作统一的写法&#xff0c;采用反射技术&#xff0c;传入参数是类的名称。 2.这里引用Java的范型T&…

低代码开发:是提高效率的美味佳肴还是无法满足深度需求的垃圾食品?

文章目录 每日一句正能量前言什么是低代码低代码的优缺点低代码开发平台优点低代码开发平台缺点 低代码会替代传统编程吗&#xff1f;低代码定位 如何入门低代码&#xff1f;后记 每日一句正能量 有志者自有千计万计&#xff0c;无志者只感千难万难。 前言 随着技术的不断发展…