【Linux】线程机制深度实践:创建、等待、互斥与同步

发布于:2025-07-14 ⋅ 阅读:(15) ⋅ 点赞:(0)

背景知识

前面我们在进行学些进程的时候,我们知道进程=内核数据结构+进程的代码和数据,其中内核数据包括PCB、程序地址空间、页表(用户级页表和内核级页表)、寄存器(其中存储进程的上下文数据)和被打开的文件和信号

操作系统的细粒度划分

数据区、代码区和栈区空间都是连续的,栈区有栈顶和栈底用于进行栈区空间的管理,代码区和数据区是如何进行数据管理和空间管理??   我们知道要想进行将数据存储到堆区中,需要进行申请空间,在进行申请空间时,我们只需要指定特定空间的大小,操作系统就可以帮我们进行堆区空间的分配,可以拿到申请到空间的起始地址,堆区看起来是整体的,但其实是malloc形成的小空间段,操作系统是如何做到对于栈区的空间管理的呢??

其实操作系统是可以让进程对资源进行细粒度划分的!!!

操作系统中通过vm_area_struct结构体进行对栈区进行,vm_area_struct结构体中通过vm_start和vm_end进行区域的空间控制的.

虚拟地址空间到物理空间的转化

过程

我们曾经学习过虚拟地址空间是通过页表进行映射到内存中的,我们现在来探讨是如何通过页表进行映射到内存中的呢??

关于磁盘

  1. .exe就是文件
  2. 我们的可执行程序就是按照程序地址空间进行编译的,可执行程序编译形成的二进制文件的格式为elf格式
  3. 可执行程序,其实按照区域已经划分成为了以4KB为单位,这个以4KB为基本大小的空间称为页帧

关于内存

  1. 内存也被划分成以4KB为基本单位的若干份小空间,这个以4KB为基本单位的若干份小空间称为页框
  2. 以32位操作系统为例,2的32次方=4,294,967,296字节=4GB,每个地址指向一个字节,32位操作系统最大的寻址为4GB,也就是说,内存最大为4GB.
  3. 4GB/4*1024*1024=1048576,按照以4KB为基本单位,能将内存划分成1048576个小单元,操作系统需要知道某个内存单元是否被使用,是否使用后被删除等等问题,操作系统需要对这100w+个内存单元进行管理(先描述在组织).

说明:以上进行的4KB为单位进行的划分为软件划分,并不是物理意义上的划分.

补充:

OS在进行IO的过程其实就是将页帧装进页表中的过程

  • 虚拟地址空间到物理内存的映射通过页表进行,我们思考,当内存单元被使用后经过删除操作后,是否需要将内存单元中的数据进行删除处理呢??

其实并不需要,进行删除数据的话效率并不高,可以通过文件系统中的思想,通过标记位进行处理,当内存单元被使用时将标记为设为1,内存单元没有被使用或者被使用后进行了删除操作,直接将内存单元的标记进行置零处理即可.

缺页中断的过程

当CPU进行寻址的过程,首先虚拟地址空间经过页表进行映射,当我们进行寻址,发现并不在内存中,然后OS系统进行在内存中申请空间,在磁盘中进行数据的查找,将数据进行加载到内存中,将内存中的地址填到页表中重新与虚拟地址空间建立联系.

页表的结构

页表也是被加载到内存中的,按照我们上面理解的方式进行计算页表占用内存的大小

32位操作系统下,共有2的32次方个地址,页表的一行称为一个条目,每个条目中需要进行9个字节的空间大小,其中4个字节大小的空间用于存储虚拟地址空间的地址,另一个4字节空间的大小存储物理内存的地址,还有1字节的空间用于存储标记位,也就是说2的32次放个地址需要2的32次方个条目,每个条目是9个字节,整个页表需要的空间是36G的空间的大小,我们的内存是远远不够的,操作系统在设计的过程中也考虑到了这个问题,通过多级页表的方式进行解决了这个问题

通过将32位的二进制序列进行分割成10位,10位,12位,其中前十位存储到一级页表进行向耳机页表进行映射,次前十位用于二级页表向内存中建立映射关系,利用一级页表和二级页表进行找到我们对应的数据映射到内存中的页框的起始地址,低12位最大表示2的12次方刚好是4KB用于特定页的数据查询

页表所占用的空间也会成指数的下降,1MB*条目(9+9)也是不到20MB.

线程的概念

线程是在进程内部进行执行,是CPU进行调度的基本单位.

线程的理解

如何理解线程(为什么说线程是进程内部执行的,是CPU调度的基本单位)

当时我们通过fork进行创建子进程的时候,每个进程都有自己的PCB结构,每个进程也有自己独立的地址空间和页表,每个线程也有自己的PCB结构,但是多个线程间是共享同一片地址空间和页表,CPU进行调度时也是将对应的task_struct加载到CPU中,然后采用时间片的方式进行调度,CPU是不关心这个PCB内核结构是进程还是线程的.

通过对于线程的理解对进程概念进行重构

重新认识task_struct

曾经我们认为一个进程只有一个PCB,task_struct是PCB的数据结构,通过线程的学习,task_struct可以理解成进程内部的一个执行流

从资源角度进行看待进程和线程

  • 用户视角

进程=内核数据结构+该进程对应的代码和数据,其中内核数据我们曾将认为一个进程只有一个PCB结构

  • 内核视角

进程向OS索要地址空间和页表,是承担分配系统资源的实体

从CPU视角进行看待进程和线程

CPU是不关系调度的是进程还是线程,CPU是只认task_struct的,其实在Linux操作系统下是没有单独去为线程设计一个独立的数据结构的,也就是说Linux系统下是没有真正意义的线程结构,线程结构是使用进程的PCB模拟的.但是其他OS是为线程设计独立的数据结构的,也就是说Linux操作系统中的线程和其他操作系统进行相比,Linux中PCB<=其他OS中的PCB的,取等的条件是单进程单线程时取等.

为什么CPU进行线程切换的成本更低的??

CPU中是存在一个缓存的,这个缓存是硬件级别的,CPU通过L1~L3cache,根据局部性原理,实现对内存的代码数据预读的CPU内部的缓存中,线程进行切换是不需要进行切换虚拟地址空间和页表的,许多数据的访问不需要再进行通过页表进行映射,直接可以再缓存中拿到,减少了访问内存的开销,但是进行进程切换时,需要进行重新加载虚拟地址空间和页表,缓存中的数据也成为无效数据,因此进程进行切换的成本更高.

线程是如何看待内部的资源的?

如果定义一个函数,在各线程 中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境: 文件描述符表 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数) 当前工作目录 用户id和组id

线程共享进程数据,但也拥有自己的一部分数据: 线程ID 一组寄存器 栈 errno 信号屏蔽字 调度优先级

多线程的用途

合理的使用多线程,能提高CPU密集型程序的执行效率

合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是 多线程运行的一种表现)

多线程的优缺点

线程异常

如何看待线程库?

线程的控制

线程的创建

pthread_creat

参数说明:

  1. *pthread_t thread

    这是一个指向 pthread_t 类型的指针,用于返回新创建的线程的 ID。pthread_t 是一个用来标识线程的类型,通常是一个无符号整数。
  2. *const pthread_attr_t attr

    这是一个指向 pthread_attr_t 类型的指针,用于指定新线程的属性。pthread_attr_t 包含线程的一些配置选项(如线程栈大小、线程优先级等)。如果不需要特定的线程属性,可以传递 NULL,表示使用默认的线程属性。
  3. void *(*start_routine)(void *)

    这是一个指向函数的指针,这个函数是新线程开始执行的代码。该函数的原型要求返回 void*,并且接受一个 void* 类型的参数。你可以通过这个参数将数据传递给线程。线程启动后,它会调用这个函数,执行相应的任务。
  4. *void arg

    这是传递给 start_routine 函数的参数。可以将任何数据(比如结构体、整数或其他指针等)传递给新线程。如果没有参数要传递,可以传递 NULL

返回值:

  • 成功:返回 0
  • 失败:返回一个错误码(例如,EAGAINENOMEMEINVAL 等)。可以通过查看 errno 获取更详细的信息。

创建一个线程

直接进行编译不成功问题

当我们进行创建一个进程时,通过g++进行编译时,没有找到pthread这个库文件,导致我们调用的pthread_create这个函数没有找到引用,这是因为pthread这个库是系统进行提供的,语言层面是默认找不到这个库的,需要进行指定加载

进行运行指定程序没有按照预期进行打印

主线程是进行打印语句了,但是新线程没有打印,这是因为线程的执行是和CPU的调度有关,CPU调度主线程进行打印完语句后执行return语句比调度新线程快

创建多个线程

多线程中出现的疑惑

  • 共享缓冲区:所有线程共享同一个全局缓冲区 buffer

  • 竞争条件:主线程在循环中不断修改 buffer 的内容,而子线程在延迟后读取 buffer 的内容。

  • 结果:由于线程执行的顺序不确定,子线程可能读取到被后续线程覆盖的 buffer 内容。

在学习过程中的误区:

当时学习到这部分的内容时,始终理解不了为什么缓冲区会被更改,当时认为主线程进行创建出来新线程的时候,缓冲区就被传给新线程了,认为每个新线程都能拿到自己缓冲区中的数据,想不明白为什么缓冲区被覆盖后,模拟线程延迟后,为什么后续新线程进行打印缓冲区中的数据是一致的

更正误区

其实在sleep(1)模拟进程进行延时的过程中,主线程已将10个新线程已经创建出来了,并且主线程已将缓冲区进行更改

处理方法

通过进行为每一个线程进行new一个对象,这个对象是每个线程独享的,通过这种形式解决共享缓冲区被覆盖的问题。

startRoutine函数的函数状态是什么?startRoutine函数是否为可重入函数?为什么??

startRoutine函数被多个线程进行访问,故函数状态为重入状态

startRoutine函数是可重入函数

验证startRoutine函数是可重入函数:

通过验证我们看到函数内定义的变量不同的线程间是不会产生影响的

为什么呢?

每个进行都有自己独立的栈结构和CPU寄存器状态

  • 独立栈:每个线程有自己独立的栈空间,用于存储局部变量、函数调用链等。

  • 独立寄存器状态:每个线程有独立的 CPU 寄存器状态(如程序计数器、栈指针等),用于保存执行上下文。

线程终止

线程中止的三种方式

  • 通过return语句进行中止

    线程虽然可以被return语句进行中止,但是不能采用exit进行中止,exit 只能进行中止进程,exit是向进程进行发送信号
  • pthread_exit函数进行中止
  • 被cancel进行取消  PTHREAD_CANCELED

    线程如果是被取消的,退出码为-1

线程等待

线程的进行等待的目的

  • 主线程需要拿到新线程的退出信息
  • 回收新线程PCB等内核资源,防止内存泄漏

线程等待的方式

pthread_join

参数说明

  • thread
    目标线程的 ID(pthread_t 类型),表示需要等待的线程。

  • retval
    指向指针的指针,用于存储目标线程的返回值。如果不需要返回值,可以传入 NULL

返回值

  • 成功:返回 0

  • 失败:返回错误码(非零值),常见的错误码包括:

    • ESRCH:指定的线程 ID 无效。

    • EINVAL:目标线程是分离线程(detached),或者已经有其他线程在等待它。

    • EDEADLK:死锁(例如,线程尝试等待自己)。

线程等待的使用


理解线程等待的参数

线程的方法中将方法的返回值通过pthread库,将线程退出的返回值给用户层拿到。

线程分离

通过上面学习我们知道线程进行退出我们是通过pthread_join进行阻塞式等待,但是我们有时候不进行关心线程的退出信息,这时候我们就可以进行通过设置线程的状态,让线程处于分离状态,这样的线程即可不被等待,线程结束后会自动进行释放其资源

线程分离的分类

  • 主线程分离新线程
  • 新线程分离新线程 

主线程分离新线程的情况,通常是在创建线程时设置属性为PTHREAD_CREATE_DETACHED,或者在创建后调用pthread_detach函数。这时候主线程不需要管理新线程的资源,适合主线程不需要等待子线程结束的情况,比如后台任务。

而新线程自己分离自己,可能是在线程函数内部调用pthread_detach(pthread_self()),这样线程一旦启动就自行分离。这种方法适用于线程自身确定不需要被其他线程等待的情况,但需要注意分离后无法再被join,所以要确保不需要获取线程的返回值。

 主线程分离新线程
  • 场景:主线程明确知道不需要等待新线程的结果,且希望新线程结束后自动释放资源。

  • 实现方式

    1. 创建时直接分离:通过设置线程属性为 PTHREAD_CREATE_DETACHED

    2. 创建后分离:主线程调用 pthread_detach(thread_id)

新线程自己分离自己
  • 场景:新线程希望自己控制资源释放,避免主线程或其他线程干预。

  • 实现方式:在线程入口函数中调用 pthread_detach(pthread_self())

拓:

原生线程库其实是调用了clone这个系统调用接口。

现在我们再重新进行看一下我们曾经进行面临的线程ID问题,我们在进行创建线程的时候,我们是通过pthread库进行创建的线程,但是pthread这个库并不是只是我们在进行使用,通过pthread进行创建的线程操作系统需要进行管理(例如是谁进行创建的这批线程),因此需要对线程进行管理,通过pthread_attr_t 对线程进行管理

我们可以发现,pthread_attr_t中的数组的大小只有56个,比较少,通过这56个char 空间的大小就可以对线程进行管理吗??其实不是,Linux实现的方案是执行流的是属性在内核中进行实现,一些用于关心的属性在库中进行实现。

先描述在组织是怎么做到的??用户级ID是什么??

主线程的栈是虚拟地址空间,新线程的栈是共享区中的空间

线程互斥

线程互斥的概念

线程互斥是在访问公共资源时,避免多个线程之间进行竞争资源导致线程安全问题,在同一时刻仅允许一个线程进行访问公共资源。

通过前面的知识进行编写一个简单的多线程抢票逻辑

代码逻辑:

通过创建若干个线程,然后让创建出来的若干个线程去执行get_tickers函数(该函数是用于模拟我们进行抢票时的逻辑),在get_tickers函数中进行通过usleep函数进行模拟线程在进行抢票过程中进行填写指定信息消耗的时间。

通过上面的代码我们可以看到,当一个全局变量被多个线程进行同时访问时,在同一时刻可能有多个线程同时进行访问并修改全局数据,当最后一个票数变成1时,此时若干个线程通过判断条件tickers>0,这时候条件是满足抢票时的逻辑的,此时若干个线程同时进行访问并处理全局数据就会出现上图情况,票变成了负数,这种情况我们称为线程数据不安全问题。

数据不安全问题

在C/C++上面的进行++,或者进行--,看起来只有一条语句,但是在汇编上至少是三条语句

①从内存中进行读取数据到CPU中

②在CPU中进行算数运算和逻辑运算

③写回新的结果在内存中的变量中

当线程A将内存中的变量的数据进行交给CPU进行处理--算术处理,当CPU进行把数据处理完成后,由进程A将CPU进行计算后的结果进行存储到内存中的变量中,假如线程A进行循环执行2次循环后,在进行执行第三次循环时,将数据放到CPU中进行计算,刚计算完成由于线程A时间片到了导致线程A无法拿着CPU中进行计算后的数据向内存变量进行写入,而是将数据存储到线程A的上下文中,然后CPU进行调度线程B,假如线程B拿着内存中被线程A进行处理过两次的数据进行循环执行5000后终于要将内存中的数据进行修改成4988时,在执行③过程后线程B也被切下,此时CPU在进行调度线程A,线程A从自己的上下文中将数据进行继续执行刚刚未执行完的操作,然后执行③过程又将线程B好不容易进行循环了5000次进行修改了的内存变量中的数据又重新变成了线程A循环两次的数据,线程A继续循环执行,相当于线程B对内存变量做的贡献完全不见了,造成数据不安全问题。

线程的封装

由于我们经常使用线程,并且线程的使用接口相对来说比较复杂,所以我们将线程进行封装。

在进行封装线程的过程中,我们在进行pthread_create()这个函数是C语言函数,我们在进行调用的时候必须通过函数指针进行调用,但是我们是进行的C/C++混编,我们是通过function进行定义的函数,这时候我们想到通过一个返回值是void* 参数为void* 的函数进行转化(如上图1),然后将函数进行传递过去,当然这里可以通过不定义function,直接通过函数指针进行,直接可以解决问题,但是通过函数进行转化是因为后续可以多分析几种场景,言归正传,通过在类中定义函数进行解决的话还是会报错,这是因为我们在类中进行进行定义函数,函数的参数实际上有两个,其中一个是this指针,另一个是我们自己定义的args,我们通过将start_routine定义成静态函数进行解决我们当时面对的问题,当然这里也可以通过友元函数进行解决,继续言归正传,通过设置成静态函数又会面临一个问题,静态函数只能调用静态成员,这里通过再构建一个类进行解决(也是非常的巧妙)如下图。

最终版本

解决数据不安全问题

互斥锁

互斥锁本身是一种同步机制,用来进行实现互斥

基于抢票程序出现的数据不安全问题对于加锁和解锁的认识

概念铺垫

  • 多个执行流共同进行安全访问的共享资源称为临界资源
  • 我们多个执行流进行访问临界资源区的代码称为临界区,临界区的代码往往是线程代码的很小一部分
  • 想让若干线程进行串行访问共享资源,称为线程互斥
  • 进行线程互斥的保证是通过原子性进行的
加锁的原理

加锁的本质就是保证进程的原子性通。过加锁的方式保证线程必须执行完上面三个步骤的操作线程才能被切下,这也就是原子性的概念,简单的说就是要么不做要么做完。

加锁是如何进行保证线程的原子性的呢??

通过创建锁,若干个线程去竞争这把锁,当某一个线程竞争到这把锁后,在这个线程持有锁到这个进程进行解锁的过程中,其他的线程无法进行访问临界资源,必须进行阻塞式等待持有锁的线程去解锁后,才可以进行去竞争锁,只有当持有锁的线程才可以去进行访问临界资源。

使用锁的步骤
  1. 初始化互斥锁

  • 静态初始化(全局/静态变量):

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

  • 动态初始化(运行时设置属性):

pthread_mutex metex 定义变量

pthread_mutex_init(&mutex,NULL)

  • 加锁于解锁

pthread_mutex_lock(&mutex); // 加锁

// 操作共享资源(临界区代码)

pthread_mutex_unlock(&mutex); // 解锁

  • 销毁互斥锁

动态初始化的锁需销毁,静态初始化的锁通常无需手动销毁:

pthread_mutex_destory(&mutex)

通过加锁进行处理数据不安全问题

通过加锁后解决了数据的不安全问题,但是加锁之后为什么全是一个进程在进行执行我们的任务呢??

其实这很好理解,我们创建了4个线程,当持有锁的线程进行解锁后,由于解锁后不需要去执行其他的代码,刚进行解锁的这个线程又去和其他线程一同进行去争夺锁,可能由于此线程的优先级高于其他几个线程导致该线程处于一直持有锁、开锁、继续持有锁的过程。但是这种情况一般不会发生,因为当持有锁的线程进行解锁后,一般还需要进行继续执行其他任务,此时就不会在同一时间和其他阻塞的线程继续开始争夺锁。

互斥锁的使用

对于锁的理解

锁是用于保护临界区的数据安全问题,既然是用于保护临界区的数据安全问题,这把锁必须被线程看到并访问,也就是说这把锁本身也是属于临界资源的。

在进行加锁和解锁的过程也是原子性的

竞争锁成功的执行流继续向后执行(访问临界区),竞争失败的执行流会阻塞

如何理解加锁和解锁的本质?

加锁的过程就是将内存中的锁进行,

将寄存器中的值和内存中mutex的值进行互换是通过一条汇编进行完成的,即使线程在没有调度完时被切走要么是还没有进行申请锁成功就被切走了,要么是持有锁一起被切走,这也就保证了线程申请锁的原子性,也保证了临界资源的安全性

解锁:

简单的使用锁并对锁进行设计封装

在C/C++混编中,我想让C语言类别系统更好的兼容C++,然后拿来做一个保护或者封装

死锁
死锁的概念

一组执行流持有自己的锁资源还要去申请别人的锁资源,由于锁是不可抢占的,互相等待对方的锁退出,从而导致代码不推进的现象。

对于死锁的理解

为什么会有死锁??

在多把锁的场景下,我们知道线程会互相等待对方锁的退出从而造成死锁,那么当只有一把锁的情况下是否会出现死锁呢??答案是当然会,当只有一把锁的时候,线程在自身持有锁的情况下,线程重复进行申请锁资源也会造成死锁。

死锁的产生逻辑:

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

线程同步

线程同步的的概念

一个线程持有锁资源,当线程被切走后也是被带着锁被切走的,当线程持有锁的时候,这个线程对于锁的竞争能力肯定是比其他线程高的,当线程释放锁后,又会进行竞争到锁,导致一个线程会出现频繁的申请锁,其他线程一直处于阻塞状态,这种阻塞状态也成为饥饿状态,线程同步就是为了解决这个问题。

线程同步是线程之间协同合作有序机制,为了解决访问公共资源导致的数据安全问题和执行顺序问题。

举个栗子

当学校开了一间自习室,但是自习室中只有一个位置,自习室的钥匙就在自习室的窗台上,先去先得,进入自习室后就可以进行反锁门,不让其他人进行打扰,卷王为了争夺这个自习室的位置,早上点就出发进入自习室进行自学,肝了4个小时,想去厕所,出门后把锁门后把钥匙又放回原位,但是卷王又心想好不容易拿到的钥匙,如果去了这个厕所就失去了资格,由于这个人距离钥匙最近,又成功拿到了钥匙,又进入自习室进行学习,由此重复,导致其他人只看到这个人一直在“申请锁” “归还锁”。

条件变量

当线程进行执行某种任务时,当遇到某个条件不满足时,要在环境变量下进行等待,这里的等待是在条件变量下进行等待,当线程满足某种条件时,条件变量进行唤醒在阻塞队列队头的线程。

条件变量的相关函数接口
  • 初始化条件变量

原型

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

参数

cond:指向要初始化的条件变量的指针。

attr:条件变量属性(通常为 NULL,表示使用默认属性)。

返回值

成功返回 0,失败返回错误码(如 EAGAIN 或 ENOMEM)。

注意事项

①条件变量可以通过静态初始化(无需调用此函数):

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

②动态分配的条件变量必须显式初始化,并在不再使用时调用 pthread_cond_destroy() 销毁。

  • 将线程进行放入等待队列

原型

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

参数

cond:要等待的条件变量。

mutex:与条件变量关联的互斥锁(调用前线程必须已持有该锁)。

返回值

成功返回 0,失败返回错误码(如 EINVAL)。

关键特性

        原子性操作:调用时自动释放 mutex 并进入阻塞,唤醒后自动重新获取 mutex

        必须与循环检查条件配合使用

        pthread_mutex_lock(&mutex);
        while (condition_is_false) 
        {
            pthread_cond_wait(&cond, &mutex);
        }
        // 条件满足后的操作
        pthread_mutex_unlock(&mutex);

        虚假唤醒:即使没有其他线程调用 signal 或 broadcast,此函数也可能返回,因此必须用 while 而非 if 检查条件。

  • 进行唤醒线程

原型

int pthread_cond_signal(pthread_cond_t *cond);

参数

cond:要发送信号的条件变量。

返回值

成功返回 0,失败返回错误码(如 EINVAL)。

使用场景

当共享资源的状态变化可能满足单个线程的条件时使用(例如缓冲区有一个空闲位置)。

需要在持有互斥锁时调用:

pthread_mutex_lock(&mutex);
// 修改条件
condition = true;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

使用条件变量的一般步骤
  1. 先对临界资源进性加锁
  2. 判断条件,如果不满足进行将线程放入到等待队列中
  3. 唤醒线程
  4. 然后对临界资源进行解锁

注意事项:

进行判断条件不使用if语句,而使用while语句

一般采用先进行唤醒线程,然后再对临界资源进行解锁

为什么判断语句使用while,而不是使用if??

先进行解锁然后进行唤醒线程和先进行唤醒线程再进行解锁有什么区别??

信号量

信号量的认识
  1. 信号量的本质就是计数器,这个计数器是用来衡量临界资源的多少的计数器
  2. 只要拥有信号量,就代表我一定能够进行访问临界资源的一部分,申请信号量的本质就是对临界资源中的一小块资源进行预定机制
  3. 线程要进行访问临界资源的某一个区域---需要进行申请信号量----想要进行申请信号量就必须能看到信号量---所以说信号量的本质就是临界资源。
  4. 通过信号量的机制我们就做到了在不进行真正访问临界资源的时候就可以知道临界资源的多少,只要申请信号量成功,就是还有资源,申请失败代表条件不允许,需要进行等待。
信号量的操作

sem_t sem=10;

  • P操作

        sem--;         //申请资源

  • V操作

        sem++;        //释放资源

注意:由于信号量的本质就是临界资源,在进行申请资源和释放信号量的时候必须进行保证原子性(通过锁进行)

POSIX信号量接口的认识

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于 线程间同步。

头文件:#include<semaphore.h>

初始化信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:

pshared:0表示线程间共享,非零表示进程间共享

value:信号量初始值

销毁信号量

int sem_destroy(sem_t *sem);

等待信号量

int sem_wait(sem_t *sem); //P()

功能:等待信号量,会将信号量的值减1

发布信号量

int sem_post(sem_t *sem);//V()

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。


网站公告

今日签到

点亮在社区的每一天
去签到