Linux系统编程5(线程概念详解)

news2025/1/24 5:31:42

线程同进程一样都是OS中非常重要的部分,线程的应用场景非常的广泛,试想我们使用的视频软件,在网络不是很好的情况下,通常会采取下载的方式,现在你很想立即观看,又想下载,于是你点击了下载并且在线观看。学过进程的你会不会想,视频软件运行后在OS内形成一个进程,有一个执行流,但下载和在线观看是两件事情,这两件事情是如何同时进行的呢?你可能会想到CPU的时间片轮转,不过曾经提到过的时间片轮转是针对进程间的切换的,下载和在线观看这两件事本身处于同一个进程内完成,你可能还会想到在这个进程内创建一个子进程,主进程负责播放,子进程负责下载,这确实是一个解决问题的方法,但是创建一个进程所带来的开销是不小的。本篇文章将会介绍另一种更加轻便的解决方案——线程,同时我们需要重新理解CPU时间片轮转的调度单位

目录

什么是线程

深入理解页表 

理解进程和线程 

实践线程操作 

线程终止

线程等待 

分离线程 

线程取消 

TCB

线程的优缺点 

优点

缺点 

C++提供的线程库 


什么是线程

按照课本上的定义,线程就是进程内部的执行流,有多个执行流就意味着一个进程可以同时进行多个操作,比如视频软件,同时具备播放视频和下载视频的功能,如果只有一个执行流,那么在播放视频时就不能同时下载视频,因为播放视频和下载视频的代码是不同的

以前我们一直认为进程是CPU的调度单位,现在我们要改变这个看法,被CPU调度意味着被CPU执行,也就是一个执行流,一个进程里可以有多个线程,线程才是CPU的调度单位。所谓的调度单位就是CPU时间片轮转时的切换单位,以前我们解释CPU时间片轮转时说的是每个进程都被分配一定的CPU执行时间,到达时间,CPU会强制切换到下一个进程,以保证每个进程都能够被执行

此时,通过线程的概念能得知,CPU时间片轮转切换的并不是进程,而是线程。但上面的话并没有说错,一是创建一个进程时,默认只有一个执行流,也就是只有一个线程,时间片轮转时可以认为是切换进程。二是在后面我们将学习到Linux其实并没有线程的概念,所谓的线程在Linux中是轻量级进程

有些懵没有关系,后面会一一解释原因

现在线程的概念先放到一边,我们接下来再次回顾曾经学习过的进程地址空间

深入理解页表 

这是笔者曾经多次提到过的进程地址空间映射图,并且说过虚拟地址空间和物理内存之间一一映射,那么大家有没有思考过这么一个问题,假设虚拟地址空间有4G大小,物理内存也是4G大,而页表是虚拟地址空间和物理地址空间的一一映射,这意味着页表自身得有8G大小的空间才能够满足虚拟地址空间和物理内存之间一一映射,要知道,页表可也得加载到内存中才能让CPU执行,照这样的映射法,物理内存连一个页表都存不了,更何况4G物理内存空间还得留1G给OS呢

可想而知,页表的映射不会像哈希表那样一一对应,要明白页表的真实构造,我们就得从物理内存的划分开始

 ​​​

实际上物理内存是按4kb为单位进行划分的,每个大小单位被称为页框,大家知道磁盘往内存中加载数据时就是以4kb大小为单位,正好能够加载到物理内存的页框中,这看似巧妙的背后是前人无数日夜的精心设计

但是这好像并没有说明页表的真实构造,别急,接着往下看

真实的页表并不是只有一张,页表里存储的也不是虚拟地址和物理地址的一一对应,页表里正真存储的是物理内存中每个页框的起始地址,一张页表里只存储指定数量的页框,整个物理内存的页框被多张页表存储着

这多张页表被页目录记录着,通过页目录可以找到每一张页表,到这里,页表的整体结构就出来了,可见,当初我们刚了解页表时,进行了很大程度的简化。但是这就结束了吗?笔者只是把页表真实的结构给描绘出来,但是并没有解释现在的页表是如何进行映射的 

 

上图是虚拟内存中的一个虚拟地址,接下来我们刨析这个虚拟地址如何通过页表最终映射到物理内存 

虚拟地址映射到物理内存的方法就在地址本身上,通过虚拟地址的前10位可以到页目录中找到该地址对应在哪个页表,找到具体的页表之后,虚拟地址的中间10位标识着该地址在物理内存的哪个页框里,找到具体的页框之后,那么最后12位想必大家已经猜出来了

最后12位正是页框内的偏移地址,因为一个页框大小就是4kb,要想在某个页框内准确定位,就要知道该页框的起始地址以及在该页框内的偏移地址。至于对不对,咱们验证一下

一个地址的大小是4字节,2的12次方是4096,4096 * 4字节 = 4kb,所以验证正确

如上,真实的页表映射结构就展现在我们眼前,笔者这里并不是心血来潮讲一下页表,通过上述的过程大家能感受到地址空间是进程接触并使用资源的窗口,页表则决定了,进程拥有哪些资源,只有页表映射到的物理内存,进程才能够访问,那么通过地址空间+页表映射进行资源划分,就可以对一个进程所用的资源进行分类

理解进程和线程 

现在回到对线程的讲解上,前面说到过线程是进程内部的执行流,一个进程可以拥有多个线程,如下图,这些线程通过使用共同的地址空间和页表从而共享进程的资源,这意味着一个进程里的多个线程共享该进程的资源

前面还提到过,CPU的基本调度单位是线程,被CPU调度执行,那就得有上下文信息,那么线程就要保存好自己的上下文信息,当被CPU切换执行时,可以将上下文信息重新载入到CPU的寄存器中。线程在共享进程资源的同时也会产生自己的执行数据,也是需要保存起来的。线程是CPU调度的基本单位,这就意味着系统中会存在大量的线程等待被CPU调用,根据以往的经验,存在大量的线程时,OS要有序将其管理起来,就得给线程设计一种数据类型,设计方法还是多次提到过的先描述,再组织

给线程设计数据类型就要考虑线程的id号在系统中唯一,同时要能存储上下文信息,线程在被CPU调度时,要有自己的状态信息,同时在执行过程中要有自己的栈结构, 并且线程共享进程资源,那么文件描述符表什么的也要有,越往下举例,就能明显感受到这不就是当初学习进程时,进程的结构里所包含的内容吗?

可以发现进程结构和线程结构大量的内容都是重叠的,如果进程和线程两种结构同时存在系统中,就会造成大量的冗余,而Linux是一个非常注重效率的OS,于是聪明的Linux设计者决定不为线程设计一个独立的结构,而是采用了轻量级进程结构,也就是说在Linux系统中,进程和线程实际上使用的是同一种结构

这一点与windows有很大的不同,windows就为线程设计了一个独立的结构,这也体现了两种OS各自的设计哲学

如何理解线程就是轻量级进程呢?如何理解现在的进程概念呢?

曾经我们认为一个task_struct就是一个进程,一个task_struct有一个执行流,并且记录着该执行信息的执行状态。现在学了线程,知道进程和线程共同使用task_struct结构,对于每个进程或线程,内核都会为其分配一个唯一的task_struct结构,现在的task_struct是一种轻量级进程,也就是说一个进程里可能含有多个task_struct,不能再将一个task_struct理解成一个进程。但这并不是说曾经学的就是错误的,曾经创建一个进程,默认有一个执行流,也就是有一个主线程,该主线程是创建进程本身的执行流,task_struct就是这个主线程的结构,故而也可以将task_struct理解成进程本身,但是多线程后,有多个task_struct,再按照以前的方法理解进程就显得不严谨了

假设现在一个进程创建了三个线程,那么会有几个task_struct呢?

如果一个进程创建了三个线程,那么通常会有四个task_struct结构,在Linux中,每个进程都有一个主线程,也就是创建该进程的线程,主线程有一个对应的task_struct结构,对于每个创建的线程,也会有一个对应的task_struct结构。 故而,对于一个进程而言,如果额外创建了三个线程,那么会有一个主线程的task_struct结构,以及三个子线程的task_struct结构,共计四个task_struct结构,这四个task_struct结构共同构成了该进程的线程组成部分

站在CPU的角度上,曾经时间片轮转时切换task_struct就是切换一个进程,现在CPU时间片轮转切换一个task_struct是切换进程的一个分支,如果这个进程只有一个主线程,那就是切换进程本身

总而言之,现在一个进程有多个执行流,进程的概念不能局限于曾经只有一个执行流的task_struct,而是一个拥有多个task_struct的承担分配系统资源的基本实体

实践线程操作 

说了这么多,咱们连线程长什么样子都不知道,接下来咱们通过实践来感受线程的魅力

不过在动手敲代码之前,需要明确一些事情,因为用轻量级进程来表示线程是Linux系统独特的线程处理方式。虽然这能很大提高效率,但是也带来了不通用的麻烦,很多OS,包括OS的理论基础上都是有线程这个概念的,因此并不通用Linux的轻量级进程,大家都在使用线程接口,而你Linux搞特殊提供轻量级进程接口,大家是不认的,为了解决这个问题,Linux工程师就将轻量级进程接口进行封装,适配成大家都通用的线程接口

这意味着,我们在使用Linux线程接口时,要在编译时带上线程动态库即选项 -l pthread

创建一个线程是通过接口

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

头文件:pthread.h   
参数
thread:返回线程ID (输出型参数)
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码

第一个参数是一个输出型参数,我们在主函数里创建一个pthread_t类型的变量,将其的地址传过去,创建线程后,会把该线程的id写入到这个pthread_t类型变量里

我们目前不需要关心 att r这个参数,可以看到第三个参数是一个函数指针,其所指向的函数就是创建一个线程后,该线程去执行的任务

第四个参数是对第三个参数的补充,在我们编写线程要执行的函数时,有时是需要外部给这个函数传参的,那个这个函数就会默认有一个void* 类型的参数,这个参数就是通过pthread_create的第四个参数传递过去的

下面看一个线程代码示例

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<cstring>
#include<cstdlib>
#include<cstdio>

using namespace std;

void* start_routine(void * arg)
{
      while(true){
        printf("%s\n", (char*)arg);
        sleep(1);
      }
}

int main()
{
    pthread_t thread_id;
    char buff[64];
    snprintf(buff, sizeof(buff), "我是新创建的线程,我正在运行");
    
    pthread_create(&thread_id, nullptr, start_routine, (void*)buff);

    int counter = 10;
    while(counter--){
        printf("我是主线程,运行倒计时:%d\n", counter);
        sleep(1);
    }

    return 0;
}

这个示例可以看到,真的有两个执行流同时在跑

通过命令ps -aL可以查看所有进程内的线程,接下来我们让两个线程不间断运行,然后查看这两个线程的相关信息

可以发现,当test程序跑起来后,出现了两个test线程,这两个线程的PID是相同的,说明这两个线程来自同一个进程,不过两个线程的LWP不同,LWP(light weight process,即轻量级进程)LWP就是所谓的线程ID了,并且第一个线程的PID和LWP相同,这说明该线程是主线程,CPU在调度时,是以LWP为标识,表示一个特定的执行流

上面只是创建单个线程,那么如何同时创建多个线程呢?

看下面的demo,我们一次创建10个线程,并且不停打印他们的序号

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

using namespace std;

#define MAX 10

void* _start_test(void* arg){

    while(true){
        sleep(1);
        cout << (char*)arg << endl;
    }
    return nullptr;
}


int main(){

    for (int i = 0; i<MAX; i++){
        pthread_t tid;
        char buff[64];
        snprintf(buff, sizeof(buff), "this is %d thread", i);
        pthread_create(&tid, nullptr, _start_test, buff);
    }

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

    return 0;
}

 

当执行结果出来后,完全超出了我们的预期,我们本想这10个线程,各自打印各自的序号,可是结果每个线程都打印序号9

出现这种情况的原因是线程被创建后的执行顺序是不确定的,当第一个被创建的进程还没来得及执行它的start_routine函数时,主线程就已经把所有的线程都创建完毕了,buff是在循环里被被创建的,出了循环后就被销毁,然后再次创建,因为都是在同一个栈里,所以每次buff的地址都不变,且buff的值不断被覆写,直到最后一个线程创建完毕,buff的值被覆写为序号9

此时循环退出,buff也被销毁了,但是由于main函数这个栈还在,也没有开其他的栈,因此原先buff指向的空间并没有被清理,导致所有的线程都打印最后一次覆写buff的内容,通过这个demo,可得知线程除了独自的PCB,独自的上下文结构,独自的栈结构,其他几乎所有内容都是共享的

每一个线程都有自己独立的栈,这是因为一个线程在执行时,可能会调用各种函数,因此需要一个独立的栈,这个栈里的内容不与其他线程共享

线程终止

会创建线程之后,自然而然的会想到,线程如何终止,导致线程终止的原因有很多

1.执行完start_routine()后,线程会自动return结束

2.使用pthread_exit()来终止当前线程,但是要注意,不要习惯性的使用exit()来终止线程,exit()是用来终止进程的,进程终止,该进程内所有的线程都会终止

3.某个线程执行过程中,出现错误,触发OS检查,会给当前线程的进程发送信号,进程收到信号会终止,该进程内其他所有线程都会终止

4. 一个线程可以调用pthread_ cancel()终止同一进程中的另一个线程

线程等待 

同进程一样,线程结束后其所申请的各种资源都是需要被回收的,不然会产生类似僵尸进程一样的问题,线程等待使用函数pthread_join()

int pthread_join(pthread_t thread, void **retval);

第一个参数就是被等待的线程id,第二个参数是获取该线程的返回值的,还记得start_routine有一个void* 返回值吗?这个返回值就是通过pthread_join获取的

注意:OS维护的是轻量级进程PCB,因为Linux特殊的线程方案,可以说没有线程概念。程序员日常使用习惯了线程接口,因此Linux提供了线程库,线程库负责线程接口与轻量级进程接口之间的转换,以及维护用户通过接口创建好的线程

分离线程 

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
使用接口:int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
pthread_detach(pthread_self());   pthread_self()获取自己的线程id

 需要注意的是,一旦一个线程已经处于分离状态,那么该线程就不能被等待

线程取消 

线程取消也就是当线程跑起来后,我们通过主线程或者其他线程可以取消这个线程继续运行

也可以自己取消自己

int pthread_cancel(pthread_t thread);

返回值:成功返回0;失败返回错误码

注意:只有当该进程运行起来,有自己的线程ID时才可以被取消

TCB

PCB是Linux内核用来管理轻量级进程的内核,因为Linux没有线程的概念,程序员要使用线程的接口,因此要通过线程库进行转接,那么程序员每申请一个线程,线程库就得维护好这个线程和轻量级进程进行转换,那么TCB就是线程库维护线程的结构

  

由图中可以得知,我们接收的所谓的线程id值其实就是库中维护的该线程TCB的起始地址 

线程的优缺点 

优点

线程的使用能非常大程度上发挥多核CPU的实力,并且创建多个线程比创建多个进程的开销要小的多,为什么呢?

如果CPU执行时,要切换一个进程,那么要切换的内容至少包含页表,虚拟地址空间,PCB,上下文数据

而切换一个线程,那么只需要切换PCB,上下文数据等主要内容

CPU在执行一个进程时,会在寄存器中缓存该进程的很多热点数据,例如虚拟地址空间,页表等,一旦切换进程,这些热点数据要全部重新加载,而切换线程,这些数据不需要动

缺点 

在运行计算密集型程序时,线程需要不停的计算,持续占有CPU,切换到其它线程的时间就会延长,导致效率低下

使用多线程编程会有互斥和同步等问题,程序的编写和维护成本很高

C++提供的线程库 

虽说Linux提供了线程库,但是Linux的线程接口和Windows下的线程接口很多都是不同的,这就导致程序的可移植性很低, C++11之后,在语言层面上对Linux和windows平台下的线程接口再进行一次封装。如此以来,用C++线程库编写的多线程程序可以同时在这两个OS平台下执行,代码的可移植性大大提高

下面的demo简单演示了如何使用C++提供的线程库,这部分属于C++的知识了,笔者将在C++专栏中介绍其详细使用方法

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

using namespace std;

void* start_routine()
{
    int counter = 10;

    while(counter--){
        sleep(1);
        cout << "我是新创建的线程,运行倒计时:" << counter <<endl;
    }

    return nullptr;
}


int main()
{
    //创建一个线程,并把执行函数传递过去
    thread t1(start_routine);

    cout << "我是主线程" <<endl;

    //主线程阻塞等待回收子线程
    t1.join();
    cout << "线程回收完毕,准备退出"<<endl;
    return 0;
}

文章的最后,大家可以尝试自己模仿C++的线程库,对Linux的线程库再进行一次封装 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/965734.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

zabbix模版和监控项

zabbix添加监控主机的流程 自定义监控项实现流程 被控端添加监控项 /etc/zabbix_agent2.d/xxx.conf UserParameterkey , 命令 ; restart服务器端测试 zabbix_get -s 主机 -k keyweb 创建模板web 在模板添加监控项web 模板关联至主机观察数据和图形 创建监控项名称 获取监控项…

Python之分支-循环

Python之分支-循环 程序控制 顺序 按照先后顺序一条条执行。 a 1 b a 1 c max(a, b) d c 100 # 这是顺序执行分支 根据不同的情况判断&#xff0c;条件满足执行某条件下的语句。 if 真(True)真执行的语句体passpassif True:pass else:pass # 单分支if语句这行的最后…

CP Autosar-Ethernet配置

文章目录 前言一、Eth层级结构介绍二、Autosar实践2.1 ETH Driver2.2 Eth InterfaceEth Interface Autosar配置2.3 TcpIp模块Eth TcpIp Autosar配置2.4 SoAdEth SoAd配置前言 因汽车E/E架构和功能的复杂度提升而带来的对车辆数据传输带宽提高和通讯方式改变(基于服务的通讯-S…

程序开发:构建功能强大的应用的艺术

程序开发是在今天的数字化时代中扮演重要角色的一项技术。通过编写代码&#xff0c;开发人员能创造出无数不同的应用&#xff0c;从简单的计算器到复杂的社交平台。电子商务应用、在线教育平台、医疗记录系统等&#xff0c;都重视程序开发的重要性&#xff0c;通过这其中的交互…

mybatis源码学习-2-项目结构

写在前面,这里会有很多借鉴的内容,有以下三个原因 本博客只是作为本人学习记录并用以分享,并不是专业的技术型博客笔者是位刚刚开始尝试阅读源码的人,对源码的阅读流程乃至整体架构并不熟悉,观看他人博客可以帮助我快速入门如果只是笔者自己观看,难免会有很多弄不懂乃至理解错误…

人工智能论文通用创新点(一)——ACMIX 卷积与注意力融合、GCnet(全局特征融合)、Coordinate_attention、SPD(可替换下采样)

1.ACMIX 卷积与注意力融合 论文地址:https://arxiv.org/pdf/2111.14556.pdf 为了实现卷积与注意力的融合,我们让特征图经过两个路径,一个路径经过卷积,另外一个路径经过Transformer,但是,现在有一个问题,卷积路径比较快,Transformer比较慢。因此,我们让Q,K,V通过1*1的…

SAP_ABAP_SCREEN_屏幕案例

SAP ABAP顾问能力模型梳理_企业数字化建设者的博客-CSDN博客SAP Abap顾问能力模型&#xff0c;ALV/REPORT|SMARTFROM|SCREEN|OLE|BAPI|BDC|PI|IDOC|RFC|API|WEBSERVICE|Enhancement|UserExits|Badi|Debughttps://blog.csdn.net/java_zhong1990/article/details/132469977 一 背…

机器视觉工程师,有哪几种类型

1.光学实验室&#xff08;打光机器视觉工程师&#xff0c;一般此职位&#xff0c;要求有光学学历的背景最佳&#xff09; 2.机器视觉算法开发工程师&#xff08;此职位国内稀缺&#xff09;3.机器视觉工程师/机器视觉开发工程师&#xff08;MV工程师/MV工程师&#xff09;&…

Unity动态设置天空盒

代码设置环境贴图 在LightingSetting面板中的设置方式 代码设置方式 RenderSettings.skybox material;

【Spring面试题】IOC控制反转和DI依赖注入(详解)

IOC Inversion of Control 控制反转&#xff0c;是一种面向对象的思想。 控制反转就是把创建和管理 bean 的过程转移给了第三方。而这个第三方&#xff0c;就是 Spring IoC Container&#xff0c;对于 IoC 来说&#xff0c;最重要的就是容器。 通俗点讲&#xff0c;因为项目…

利用python制作AI图片优化工具

将模糊图片4K高清化效果如下&#xff1a; 优化前的图片 优化后如下图&#xff1a; 优化后图片变大变清晰了效果很明显 软件界面如下&#xff1a; 所用工具和代码&#xff1a; 1、所需软件包 网盘链接&#xff1a;https://pan.baidu.com/s/1CMvn4Y7edDTR4COfu4FviA提取码&am…

Yolov5 中添加注意力机制 CBAM

Yolov5 中添加注意力机制 CBAM 1. CBAM1.1 Channel Attention Module1.2 Spatial Attention Module1.3 Channel attention 和 Spatial attention 如何去使用 2. 在Yolov5中添加CBAM模块2.1 修改common.py 文件2.2 修改yolo.py 文件2.3 修改网络配置yolov5x-seg.yaml文件 3. 训练…

TCP Header都有啥?

分析&回答 源端口号&#xff08;Source Port&#xff09; &#xff1a;16位&#xff0c;标识主机上发起传送的应用程序&#xff1b; 目的端口&#xff08;Destonation Port&#xff09; &#xff1a;16位&#xff0c;标识主机上传送要到达的应用程序。 源端&#xff0c;目…

WSL中为Ubuntu和Debian设置固定IP的终极指南

文章目录 **WSL中为Ubuntu和Debian设置固定IP的终极指南****引言/背景****1. 传统方法****2. 新方法:添加指定IP而不是更改IP****结论**WSL中为Ubuntu和Debian设置固定IP的终极指南 引言/背景 随着WSL(Windows Subsystem for Linux)的普及,越来越多的开发者开始在Windows…

网络防火墙与入侵检测系统(IDS/IPS):深入研究现代防火墙和IDS/IPS技术,提供配置和管理建议

第一章&#xff1a;引言 随着信息技术的飞速发展&#xff0c;网络安全的重要性日益凸显。在这个充满威胁的数字时代&#xff0c;网络防火墙和入侵检测系统&#xff08;IDS/IPS&#xff09;成为保护企业和个人免受网络攻击的关键工具。本文将深入研究现代防火墙和IDS/IPS技术&a…

第9章 函数

本章介绍以下内容&#xff1a; 关键字&#xff1a;return 运算符&#xff1a;*&#xff08;一元&#xff09;、&&#xff08;一元&#xff09; 函数及其定义方式 如何使用参数和返回值 如何把指针变量用作函数参数 函数类型 ANSI C原型 递归 如何组织程序&#xff1f;C的设…

MongoDB 的简介

MongoDB 趋势 对于 MongoDB 的认识 Q&A QA什么是 MongoDB&#xff1f; 一个以 JSON 为数据模型的文档数据库一个以 JSON 为数据模型的文档数据库文档来自于“JSON Document”&#xff0c;并非我们一般理解的 PDF&#xff0c;WORD谁开发 MongDB&#xff1f; 上市公司 MongoD…

POI-TL制作word

本文相当于笔记&#xff0c;主要根据官方文档Poi-tl Documentation和poi-tl的使用&#xff08;最全详解&#xff09;_JavaSupeMan的博客-CSDN博客文章进行学习&#xff08;上班够用&#xff09; Data AllArgsConstructor NoArgsConstructor ToString EqualsAndHashCode public …

抽象轻松c语言

目 c语言 c程序 c语言的核心在于语言&#xff0c;语言的作用是进行沟通&#xff0c;人与人之间的信息交换 人与人之间的信息交换是会有信息空白&#xff08;A表达信息&#xff0c;B接受信息&#xff0c;B对信息的处理会与A所以表达的信息具有差距&#xff0c;这段差距称为信…

【4-5章】Spark编程基础(Python版)

课程资源&#xff1a;&#xff08;林子雨&#xff09;Spark编程基础(Python版)_哔哩哔哩_bilibili 第4章 RDD编程&#xff08;21节&#xff09; Spark生态系统&#xff1a; Spark Core&#xff1a;底层核心&#xff08;RDD编程是针对这个&#xff09;Spark SQL&#xff1a;…