Linux线程【1概念】

 

目录

前言:

正文:

1.什么是线程?

1.1基本概念

1.2线程理解 

1.3进程与线程的关系

1.4线程使用

2.重谈地址空间 

 2.1页表的大小

2.2、内存与磁盘的交互

2.3深入页表 

2.4小结

3.线程概念总结 

3.1、再谈线程

3.2线程的优点 

 3.3线程缺点

3.4线程用途 



前言:

一份代码成功编译后,成为可执行程序,运行起来,相关代码和数据被加载到内存中,操作系统为了管理进程,会先描述在组织,难道只有进程能够完成用户想要执行的任务吗?如果任务很大呢?为了满足高效运行的需求,需要一种执行力度更细的,成本更低的执行流,在linux操作系统中将这样的执行流称之为-线程 

 windos 中线程数据

 

正文:

1.什么是线程?

1.1基本概念

教材 概念:

  1. 线程就是一个执行分支、执行粒度比进程更细、调度成本更低
  2. 线程就是进程内部的一个执行流

内核角度:

  • 进程是承担系统资源分配的基本实体,而线程是 CPU 运行的基本单位

线程是对以往进程概念的补充完善,正确理解线程概念是一件十分重要的事

1.2线程理解 

理解 线程 之前需要先简单回顾一下 进程

  • 程序运行后,相关的代码和数据会被 load 到内存中,然后操作系统为其创建对应的 PCB 数据结构、生成虚拟地址空间、分配对应的资源,并通过页表建立映射关系

进程之间是相互独立

即使是 父子进程,他们也有各自的 虚拟地址空间、映射关系、代码和数据(可能共享部分数据,出现修改行为时引发 写时拷贝机制)

如果我们想要创建 其他进程 执行任务,那么 虚拟地址空间、映射关系、代码和数据 这几样东西是必不可少的,想象一下:如果只有进程的概念,并且同时存在几百个进程,那么操作系统调度就会变得十分臃肿

 

  • 对于频繁IO操作,系统在调度进程时,需要频繁保存上下文数据、创建的虚拟地址空间及建立映射关系

为了避免这种繁琐的操作,引入了 线程 的概念,所谓 线程 就是:额外创建一个 task_struct 结构,该 task_struct 同样指向当前的虚拟地址空间,并且不需要建立映射关系及加载代码和数据,如此一来,操作系统只需要 针对一个 task_struct 结构即可完成调度,成本非常低

为什么切换进程比切换线程开销大得多?
在 CPU 内部包括:运算器、控制器、寄存器、MMU、硬件级缓存(cache),其中 硬件级缓存 cache 又称为 高速缓存,遵循计算机设计的基本原则:局部性原理,会预先加载 部分用户可能访问的数据 以提高效率,如果切换进程,会导致 高速缓存 中的数据无法使用(进程具有独立性),重新开始 预加载,这是非常浪费时间的(对于 CPU 来说);但切换线程就不一样了,因此线程从属于进程,切换线程时,所需要的数据的不会发生改变,这就意味值 高数缓存 中的数据可以继续使用,并且可以接着 预加载 下一波数据

 注:高速缓存中预加载的是公共数据,并非线程的私有数据

 

进程(process)的 task_struct 称为 PCB,线程(thread)的 task_struct 则称为 TCB

从今天开始,无论是 进程 还是 线程,都可以称为 执行流,线程 从属于 进程:当进程中只有一个线程时,我们可以粗粒度的称当前进程为一个单独的执行流;当进程中有多个线程时,则称当前进程为多执行流,其中每一个执行流就是一个个的线程

执行流的调度由操作系统负责,CPU 只负责根据 task_struct 结构进行计算

  • 若下一个待调度的执行流为一个单独的进程,操作系统仍需创建 PCB 及 虚拟地址空间、建立映射关系、加载代码和数据
  • 但如果下一个待调度的执行流为一个线程,操作系统只需要创建一个 TCB,并将其指向已有的虚拟地址空间即可

1.3进程与线程的关系

        进程是承担系统资源分配的实体,比如 程序运行必备的:虚拟地址空间、页表映射关系、相关数据和代码 这些都是存储在 进程 中的,也就是我们历史学习中 进程 的基本概念

        线程是 CPU 运行的基本单位,程序运行时,CPU 只认 task_struct 结构,并不关心你是 线程 还是 进程,不过,线程 包含于 进程 中,一个 进程 可以只有一个 线程,也可以有很多 线程,当只有一个 线程 时,通常将其称为 进程,但对于 CPU 来说,这个 进程 本质上仍然是 线程;因为 CPU 只认 task_struct 结构,并且 PCB 与 TCB 都属于 task_strcut,所以才说 线程是 CPU 运行的基本单位

总结:进程是由操作系统将程序运行所需地址空间、映射关系、代码和数据打包后的资源包,而 线程/轻量级线程/执行流 则是利用资源完成任务的基本单位

线程包含于进程中,进程本身也是一个线程

我们之前学习的进程概念是不完整的,引入线程之后,可以对进程有一个更加全面的认识

通常将程序启动,比如 main 函数中的这个线程称为 主线程,其他线程则称为 次线程

实际上一个完整的进程 = 内核PCB+虚拟地址空间+映射关系+代码和数据,同时线程又分割进程除了映射代码和数据以外的部分

 

 

在 Linux 中,认为 PCB 与 TCB 的共同点太多了,于是直接复用了 PCB 的设计思想和调度策略,在进行 线程管理 时,完全可以复用 进程管理 的解决方案(代码和结构),这可以大大减少系统调度时的开销,做到 小而美,因此 Linux 中实际是没有真正的 线程 概念的,有的只是复用 PCB 设计思想的 TCB

在这种设计思想下,线程 注定不会过于庞大,因此 Linux 中的 线程 又可以称为 轻量级进程(LWP),轻量级进程 足够简单,且 易于维护、效率更高、安全性更强,可以使得 Linux 系统不间断的运行程序,不会轻易 崩溃

与 一切皆文件一样,这种设计思想注定 Linux 会成为一款 卓越 的操作系通

1.4线程使用

如何验证 Linux 中的线程解决方案? 简单使用一下就好了

接下来简单使用一下 pthread 线程原生库中的线程相关函数(只是简单使用,不涉及其他操作)

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void *threadHandler1(void *args)
{
    while (true)
    {
        cout << "我是次线程1,我正在运行..." << endl;
        sleep(1);
    }
}

void *threadHandler2(void *args)
{
    while (true)
    {
        cout << "我是次线程2,我正在运行..." << endl;
        sleep(1);
    }
}

void *threadHandler3(void *args)
{
    while (true)
    {
        cout << "我是次线程3,我正在运行..." << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t t1, t2, t3; // 创建三个线程
    pthread_create(&t1, NULL, threadHandler1, NULL);
    pthread_create(&t2, NULL, threadHandler2, NULL);
    pthread_create(&t3, NULL, threadHandler3, NULL);

    // 主线程运行
    while (true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }

    return 0;
}

编译程序时,需要带上 -lpthread 指明使用 线程原生库

结果:主线程+三个次线程同时在运行

至于为什么打印结果会有点不符合预期,这就涉及到 加锁 相关问题了,后面再解决

使用指令查看当前系统中正在运行的 线程 信息

ps -aL | head -1 && ps -aL | grep myThread | grep -v grep

 

可以看到此时有 四个线程

  • 细节1:四个线程的 PID 都是 13039
  • 细节2:四个线程的 LWP 各不相同
  • 细节3:第一个线程的 PID 和 LWP 是一样的

其中,第一个线程就是 主线程,也就是我们之前一直很熟悉的 进程,因为它的 PID 和 LWP 是一样的,所以只需要关心 PID 也行

操作系统如何判断调度时,是切换为 线程 还是切换为 进程 ?

  • 将待切换的执行流 PID 与当前执行流的 PID 进行比对,如果相同,说明接下来要切换的是 线程,否则切换的就是 进程
  • 操作系统只需要找到 LWP 与 PID 相同的线程,即可轻松锁定主线程

线程是进程的一部分,给其中任何一个线程发送信号,都会影响到其他线程,进而影响到整个进程


2.重谈地址空间 

 2.1页表的大小

 页表 是用来将 虚拟地址 和 物理地址 之间建立映射关系的,除此之外,页表 中还存在 其他属性 字段

众所周知,在 32 位系统中,存在 2^32 个地址(一个内存单元大小是 1byte),意味着虚拟地址空间 的大小为 4GB

假设极端情况:每个地址都在页表中建立了映射关系,其中页表的每一列大小都是 4 字节,那么页表的大小就是 2^32 * 4 * 3 * 1byte = 48GB,这就意味着悲观情况下页表已经干掉 48GB 的内存了,但现在电脑普遍都只有 16GB 内存,更何况是几十年前的电脑

所以页表的地址映射肯定不是这样的。

2.2、内存与磁盘的交互


操作系统从 磁盘 中读取数据时,一次读取大量数据 比 多次读取少量数据 要快的多,因为 磁盘 是外设,每一次读取都必然伴随着寻址等机械运动(机械硬盘),无论是对于 内存 还是 CPU ,这都是非常慢的,为了尽可能提高效率,操作系统选择一次 IO 大量数据的方式读取数据

通常 IO 的数据以 块 为基本单位,在文件系统中,一个 块 的大小为 4KB(一个块由8个扇区组成,单个扇区大小为 512Byte),即使我们一次只想获取一个字节,操作系统最低也会 IO 一个 数据块(4KB)

4KB 这个大小很关键

  • 文件系统/编译器:文件存储时,需要以 4KB 为单位进行存储
  • 操作系统/内存:读取文件或进行内存管理时,也是以 4KB 为单位的

也就是说,内存实际上是被切成大小为 4KB 的小块的,在内存中,单块内存(4KB)被称为 页 Page,组成单块内存的边界(类似于下标)被称为 页框(页帧)

为了将内存中的 页 Page 进行管理,需要 先描述,在组织,构建 struct page 结构体,用于描述 页 Page 的状态,比如是否为脏数据、是否已经被占用了,因为存在很多 页 Page,所以需要将这些 struct page 结构进行管理,使用的就是 数组(天然有下标) struct page mem[N],其中 N 表示当前内存中的 页 Page 数量

struct page
{
	int status; // 基础字段:状态
	// 注意:这个结构不能设计的太复杂了,因为稍微大一点内存就爆了,所以里面的属性非常少
};

struct page mem[N]; // 管理 page 结构体的数组

 

假设我们的内存为 4GB,那么等分为 4KB 的 页 Page,可以得到约 100w 个 页 Page,其中 struct page 结构体不会设计的很大,大小是 字节 级别的,也就是说 struct page mem[100w] 占用的总大小不过 4~5MB,对于偌大的内存来说可以忽略不计

内存管理的本质:

  • 申请:无非就是寻找 mem 数组中一块未被使用的足量空间,将对应的 页 Page 属性设置为已被申请,并返回起始地址(足量空间页框的起始地址)
  • 使用:将磁盘中的指定的 4KB 大小数据块存储至内存中对应的 页 Page 中
  • 释放:将 页 Page 属性设置为可用状态

关于 mem 数组的查找算法(内存分配算法):LRU、伙伴系统等

重新审视 4KB,为什么内存与磁盘交互的基本单位是 块(4KB

这里就要提一下 局部性原理 了

  • 现代计算机预加载的理论基础
  • 允许我们提前加载正在访问数据的 相邻或者附加的数据(数据预加载)

所以现在就可以回答为什么是 4KB

  1. IO 的基本单位,内核系统/文件系统 都对其提供了支持
  2. 利于通过 局部性原理 预测数据的命中情况,尽可能提高效率

总结:IO 的基本单位是 4KB,内存实际上被划分成了很多个 4KB 的小块,并存在相应的数据结构对其进行管理

2.3深入页表 

 

虚拟地址(32 位操作系统) 大小也就是 32 比特位,大概也就是 4Byte,通常将一个 虚拟地址 分割为三份:10、10、12

10:虚拟地址中的前 10 个比特位,用于寻址 页表2
10:虚拟地址中间的 10 个比特位,用于寻找 页框起始地址
12:虚拟地址中的后 12 个比特位,用于定位 具体地址(偏移量)

注:“页表2” 中的 20 表示内存中的下标,即 页框地址

通常将 “页表1” 称为 页目录,“页表2” 称为 页表项

  • 页目录:使用 10 个比特位定位 页表项
  • 页表项:使用 10 个比特位定位 页框地址
  • 偏移量:使用 12 个比特位,在 页 Page 中进行任意地址的寻址

像这种 页框起始地址+偏移量 的方式称为 基地址+偏移量,是一种运用十分广泛的思想,比如所谓的 类型(int、double、char…)都是通过 类型的起始地址+类型的大小 来标识该变量大小的,也就是说我们只需要 获得变量的起始地址,即可自由进行偏移操作(如果偏移过度了,就是越界),这也就解释了为什么取地址只会取到 起始地址

扩展:动态内存管理

实际上,我们在进行 动态内存管理(malloc/new) 申请堆空间时,操作系统 并没有立即在物理内存中申请空间(因为你申请了可能不会立马使用),而是 先在 虚拟地址 中进行申请(成本很低),当我们实际使用该空间时,操作系统 再去 填充相应的页表信息+申请具体的物理内存

像这种操作系统赌博式的行为我们已经不是第一次见了,比如之前的 写时拷贝,就是在赌你不会修改,这样做的好处就是可以 最大化提高效率,对于内存来说,这种使用时再申请的行为会引发 缺页中断

 

同理,在进行 磁盘文件读取 时,也存在 缺页中断 行为,毕竟你打开文件了,并不是立即进行读写操作的

诸如这种 硬件级的中断行为 我们已经在 信号产生 中学过了,即:从键盘按下的那一刻,发出硬件中断信号,中断控制器识别为 键盘 发出的信号后,去 中断向量表 中查找执行方法,也就是 键盘 的读取方法

所以操作系统根本不需要关系 硬件 是什么样子,只需要关心对方是否发出了 信号(请求),并作出相应的 动作(执行方法) 即可,很好的实现了 解耦

对于 内存 的具体情况,诸如:是否命中、是否被占用、对应的 RWX 权限 需要额外的空间对其进行描述,而 页表 中的 其他属性 列就包含了这些信息

 

2.4小结

 所以目前 地址空间 的所有组成部分我们都已经打通了,再次回顾这种设计时,会发现 用户压根不知道、也不需要知道虚拟地址空间之后发生的事,只需要正常使用就好了,当引发异常操作时,操作系统能在 查页表 阶段就进行拦截,而不是等到真正影响到 物理内存 时才报错

这种设计思想就是计算机界著名的 所有问题都可以通过添加一层 软件层 解决,这种思想早在几十年前就已经得到了运用

这种分层结构不仅适用于 操作系统,还适用于 网络,比如大名鼎鼎的 OSI 七层网络模型

3.线程概念总结 

3.1、再谈线程


Linux 中没有 真线程,有的只是复刻 进程 代码和管理逻辑的 轻量级线程(LWP)

线程 有以下概念:

在一个程序中的一个执行路线就叫做 线程(Thread),或者说 线程 是一个进程内部的控制程序
每一个进程都至少包含一个 主线程
线程 在进程内部执行,本质上仍然是在进程地址空间内运行
在 Linux 系统中,CPU 看到的 线程 TCB 比传统的 进程 PCB 更加轻量化
透过进程地址空间,可以看到进程的大部分资源,将资源合理分配给每个执行流,就形成了 线程执行流

3.2线程的优点 

线程 最大的优点就是 轻巧、灵活,更容易进行调度

创建一个线程的代价比创建一个进程的代价要小得多
调度线程比调度进程要容易得多
线程占用的系统资源远小于进程
可以充分利用多处理器的并行数量(进程也可以)
在等待慢速 IO 操作时,程序可以执行其他任务(比如看剧软件中的 “边下边看” 功能)
对于计算密集型应用,可以将计算分解到多个线程中实现(比如 压缩/解压 时涉及大量计算)
对于 IO密集型应用,为了提高性能,将 IO操作重叠,线程可以同时等待资源,进行 高效IO(比如 文件/网络 的大量 IO 需要,可以通过 多路转接 技术,提高效率)

 3.3线程缺点

 

1、性能损失,当 线程 数量过多时,频繁的 线程 调度所造成的消耗会导致 计算密集型应用 无法专心计算,从而造成性能损失

2、 健壮性降低,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的

为什么 单个线程 引发的错误需要让 整个进程 来承担?

站在技术角度,完全可以让其自行承担,但这不合理
系统角度:线程是进程的执行分支,线程出问题了,进程也不应该继续运行(比如一颗老鼠屎坏了一锅汤)
信号角度:线程出现异常后,MMU 识别到异常 -> 操作系统将异常转换为信号 -> 发送信号给指定进程,信号的对象是进程,自然无法单发给 线程,进而整个进程也就都终止了

3、缺乏访问控制,进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响

3.4线程用途 

合理的使用 多线程,可以提高 CPU 计算密集型程序的效率

合理的使用 多线程,可以提高 IO 密集型程序中用户的体验(具体表现为用户可以一边下载,一边做其他事情)

 

  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
同步概念 所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式略有不同。如,设备同步,是指在两个设备之间规定一个共同的时间参考;数据库同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持一致;文件同步,是指让两个或多个文件夹里的文件保持一致。等等 而,编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。 线程同步 同步即协同步调,按预定的先后次序运行。 线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。 举例1: 银行存款 5000。柜台,折:取3000;提款机,卡:取 3000。剩余:2000 举例2: 内存中100字节,线程T1欲填入全1, 线程T2欲填入全0。但如果T1执行了50个字节失去cpu,T2执行,会将T1写过的内容覆盖。当T1再次获得cpu继续 从失去cpu的位置向后写入1,当执行结束,内存中的100字节,既不是全1,也不是全0。 产生的现象叫做“与时间有关的错误”(time related)。为了避免这种数据混乱,线程需要同步。 “同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。 因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。 数据混乱原因: 1. 资源共享(独享资源则不会) 2. 调度随机(意味着数据访问会出现竞争) 3. 线程间缺乏必要的同步机制。 以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。 所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。 互斥量mutex Linux中提供一把互斥锁mutex(也称之为互斥量)。 每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。 资源还是共享的,线程间也还是竞争的, 但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。 但,应注意:同一时刻,只能有一个线程持有该锁。 当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。 所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。 因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。 主要应用函数: pthread_mutex_init函数 pthread_mutex_destroy函数 pthread_mutex_lock函数 pthread_mutex_trylock函数 pthread_mutex_unlock函数 以上5个函数的返回值都是:成功返回0, 失败返回错误号。 pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。 pthread_mutex_t mutex; 变量mutex只有两种取值1、0。 pthread_mutex_init函数 初始化一个互斥锁(互斥量) ---> 初值可看作1 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 参1:传出参数,调用时应传 &mutex restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改 参2:互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。 参APUE.12.4同步属性 1. 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。e.g. pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER; 2. 动态初始化:局部变量应采用动态初始化。e.g. pthread_mutex_init(&mutex, NULL) pthread_mutex_destroy函数 销毁一个互斥锁 int pthread_mutex_destroy(pthread_mutex_t *mutex); pthread_mutex_lock函数 加锁。可理解为将mutex--(或-1) int pthread_mutex_lock(pthread_mutex_t *mutex); pthread_mutex_unlock函数 解锁。可理解为将mutex ++(或+1) int pthread_mutex_unlock(pthread_mutex_t *mutex); pthread_mutex_trylock函数 尝试加锁 int pthread_mutex_trylock(pthread_mutex_t *mutex); 加锁与解锁 lock与unlock: lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。 unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。 例如:T1 T2 T3 T4 使用一把mutex锁。T1加锁成功,其他线程均阻塞,直至T1解锁。T1解锁后,T2 T3 T4均被唤醒,并自动再次尝试加锁。 可假想mutex锁 init成功初值为1。 lock 功能是将mutex--。 unlock将mutex++ lock与trylock: lock加锁失败会阻塞,等待锁释放。 trylock加锁失败直接返回错误号(如:EBUSY),不阻塞。 加锁步骤测试: 看如下程序:该程序是非常典型的,由于共享、竞争而没有加任何同步机制,导致产生于时间有关的错误,造成数据混乱: #include #include #include void *tfn(void *arg) { srand(time(NULL)); while (1) { printf("hello "); sleep(rand() % 3); /*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/ printf("world\n"); sleep(rand() % 3); } return NULL; } int main(void) { pthread_t tid; srand(time(NULL)); pthread_create(&tid, NULL, tfn, NULL); while (1) { printf("HELLO "); sleep(rand() % 3); printf("WORLD\n"); sleep(rand() % 3); } pthread_join(tid, NULL); return 0; } 【mutex.c】 【练习】:修改该程序,使用mutex互斥锁进行同步。 1. 定义全局互斥量,初始化init(&m, NULL)互斥量,添加对应的destry 2. 两个线程while中,两次printf前后,分别加lock和unlock 3. 将unlock挪至第二个sleep后,发现交替现象很难出现。 线程在操作完共享资源后本应该立即解锁,但修改后,线程抱着锁睡眠。睡醒解锁后又立即加锁,这两个库函数本身不会阻塞。 所以在这两行代码之间失去cpu的概率很小。因此,另外一个线程很难得到加锁的机会。 4. main 中加flag = 5 将flg在while中-- 这时,主线程输出5次后试图销毁锁,但子线程未将锁释放,无法完成。 5. main 中加pthread_cancel()将子线程取消。 【pthrd_mutex.c】 结论: 在访问共享资源前加锁,访问结束后立即解锁。锁的“粒度”应越小越好。 死锁 1. 线程试图对同一个互斥量A加锁两次。 2. 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁 【作业】:编写程序,实现上述两种死锁现象。 读写锁 与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。 读写锁状态: 一把读写锁具备三种状态: 1. 读模式下加锁状态 (读锁) 2. 写模式下加锁状态 (写锁) 3. 不加锁状态 读写锁特性: 1. 读写锁是“写模式加锁”时, 解锁前,所有对该锁加锁的线程都会被阻塞。 2. 读写锁是“读模式加锁”时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。 3. 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高 读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。 读写锁非常适合于对数据结构读的次数远大于写的情况。 主要应用函数: pthread_rwlock_init函数 pthread_rwlock_destroy函数 pthread_rwlock_rdlock函数 pthread_rwlock_wrlock函数 pthread_rwlock_tryrdlock函数 pthread_rwlock_trywrlock函数 pthread_rwlock_unlock函数 以上7 个函数的返回值都是:成功返回0, 失败直接返回错误号。 pthread_rwlock_t类型 用于定义一个读写锁变量。 pthread_rwlock_t rwlock; pthread_rwlock_init函数 初始化一把读写锁 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); 参2:attr表读写锁属性,通常使用默认属性,传NULL即可。 pthread_rwlock_destroy函数 销毁一把读写锁 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); pthread_rwlock_rdlock函数 以读方式请求读写锁。(常简称为:请求读锁) int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); pthread_rwlock_wrlock函数 以写方式请求读写锁。(常简称为:请求写锁) int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); pthread_rwlock_unlock函数 解锁 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); pthread_rwlock_tryrdlock函数 非阻塞以读方式请求读写锁(非阻塞请求读锁) int pthread_

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值