多线程的高手——海王(浅谈线程概念)

听到大海的声音了吗

让我想想炉石里面能被成为海王的卡牌

我个人感觉

就是拿着三叉戟的甲壳元素

可是这牌被删了我心痛

 背景知识

 还是地址空间那点破事!

OS进行内存管理,不是以字节为单位的,而是以内存块为单位的!

默认是4kb捏(Linux主流操作系统默认大小)

系统和磁盘文件进行IO的基本单位是4KB,也就是八个扇区

计算机中的所有巧合,都是被精心设计过的

内存是存储的空间

那4KB也可以被称为页框或者页帧,OS对内存的管理工作基本单位是4KB

拷贝的时候也是把所有的页框都拷贝过去,这是用空间换取时间的一种做法

struct page
{
    int flag;     //是否被占用,是否是脏页,是否被锁定
    int mode;
    
    ...
}

怎么组织呢?用数组管理捏!

struct page memory[1048576];

用下标就可以知道每一个内存页框的地址

虚拟地址是怎么转换成物理地址的呢?

比如说,前十个,中十个,后十个,大后十个。。。

于是就这样,史、史、史。。。

虚拟地址在操作系统看来把它拆成了

10/10/12

难以评价 

页表也不是只有一张的

前十个的数据范围是2的十次方

第一个页表有1024个元素,会以虚拟地址的前10个bit为作为索引来查第一张表

这张页表被称为页目录

页目录里面的东西还要指向页表(页表本质是搜索页框),页目录中存放的是二级页表的地址,而页表中存放的是指向页框的起始地址,而这个+12[0,4095]就叫做页内偏移!

有一组寄存器:

CR寄存器,其中的CR2寄存器是页故障寄存器,保存最后一次出现页故障的全32位线性地址

 假设正文代码中有二十个函数,将他们拆分一下

在技术上有没可行性呢?

你看我给你分析,函数有地址,每一行代码都有地址,而且同一个函数我们认为其地址是连续的

函数是什么呢?

是连续的代码地址构成代码块,一个函数对应一批连续的虚拟地址

虚拟地址的本质是一种资源

线程的概念/Linux中线程的实现

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

还是那几个固定成员:task_struct、地址空间、页表、物理内存

但是我们创建进程的时候如果不再创建物理空间和页表

就是再创建个task_struct出来,但是不额外创建地址空间和页表,而是去指向之前已经创建好的地址空间和页表,而正文代码被分成不同的部分交给这些伙伴去执行,这些伙伴共享这个进程的地址空间,而这个伙伴就叫线程!

tips:

在一个程序里的一个执行路线就叫做线程(thread)

线程是“一个进程内部的控制序列”

一切进程至少都有一个执行线程,线程在进程内部运行,本质是在进程地址空间内运行

在Linux系统中,在CPU眼中,PCB都要比传统的进程更加轻量化,透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

 

进程=内核数据结构+进程代码和数据

内核上的观点认为励志轩可以玩炉石传说

进程是承担分配系统资源的基本实体

 

EPI人魅力时刻 

哦?还有高手?

拿故事来举例的话就是家庭是进程,而家中的每个人就是线程,上学的上学,上班的上班,养老的养老,各司其职,但最终目的都是好好过日子

之前的进程是内部有一个执行流的进程

而现在的多线程的进程是内部有多个执行流的进程

 

如果OS要单独设计线程,那么这个线程是否要被新建?暂停?销毁?调度?

线程要不要和进程产生关联捏?

如果设计出了线程,那是要设计出线程的id、优先级、上下文、链接属性

进程有进程控制块,而线程也有线程控制块,而线程的PCB也要和进程的关联起来

Windows提供了真实的线程控制块

那能否复用PCB,用PCB统一表示执行流的概念呢?

这样就无需为线程单独设计数据结构和调度算法了,而Windows是存在设计方案PCB的

Windows既要维护进程也要维护线程,Linux是进行了代码的复用,在这样的情况下,Linux的设计是略优于Windows的(Linux用进程模拟线程)

那CPU要不要区分task_struct是进程还是线程?

不需要,和它无瓜。。。不做区分,都是执行流

CPU看到的执行流<=进程,都叫轻量级进程

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

进程是承担分配系统资源的基本实体

在Linux上创建线程要用这样的接口:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);

第三个参数是函数指针,是要让新线程执行的方法,第一个参数保存创建完的线程的id

那我们写一段创建新线程的代码吧:

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

// 新线程
void *threadStart(void *args)
{
    while (true)
    {
        sleep(1);
        std::cout << "new thread running..." << std::endl;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadStart, (void *)"thread-new");

    // 主线程
    while (true)
    {
        sleep(1);
        std::cout << "main thread running..." << std::endl;
    }
    return 0;
}

而线程在编译的时候需要带上线程库的选项 

testthread:testthread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f testthread

 为了保证我们看到的是多线程,就让他们把pid都告诉我们一下

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

// 新线程
void *threadStart(void *args)
{
    while (true)
    {
        sleep(1);
        std::cout << "new thread running..." << ",pid: " << getpid() << std::endl;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadStart, (void *)"thread-new");

    // 主线程
    while (true)
    {
        sleep(1);
        std::cout << "main thread running..." << ",pid: " << getpid() << std::endl;
    }
    return 0;
}

 

可以看到虽然是两个执行流,但是只有一个进程

 怎样查看线程捏?

ps -aL

 

那LWP是什么?

是轻量级进程(Light Weight Process)

那OS调度的时候看的是pid还是LWP?

 肯定是LWP呀

PID和LWP一样的是主线程

有一些问题:

已经有多进程了,为什么还要有多线程?

进程的创建成本非常高!

地址空间、页表、物理内存...

而线程创建的成本比较低(启动),线程的调度成本也低(运行)

那删除一个线程呢?

也比较方便,进程具有独立性,但是线程间会影响,一个线程挂了其他线程会受到影响

为什么说线程的调度成本更低?

首先地址空间是共享的,是同一张页表

CPU在调度进程的时候

不同系统对于进程和线程的实现是不一样的

CPU内存在着cache,我们的代码会被预先加载到cache当中,那是热数据,cache是集成在CPU中的硬件

炉石玩家传统美德

从哪可以查到呢?

其实我们 lscpu 就可以查看CPU的属性

如果我们是进程间切换

每个进程作用不一样,要再进行切换,所以cache里的数据就都要重新换

但是线程在切换的时候就不用,因为cache上的数据是可能被用上的

那如果进程执行的代码就是一款OS呢?(不是这什么新赛道)

这就是虚拟机啊(一个进程挂掉也不会影响另一个,不是哥们?)

透过进程的虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流,而线程也不是越多越好,是合适的才是最好的

如果是两核的CPU那就创建两个线程比较好

线程优点

总结线程优点:

☃ 创建一个新线程的代价要比创建一个新进程小得多

☃ 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

☃ 线程占用的资源要比进程少很多

☃ 能充分利用多处理器的可并行数量

☃ 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

☃ 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

☃ I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

线程缺点

性能损失

        一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,性能损失指的是增加了额外的同步和调度开销,而可用的资源不变

健壮性降低

        编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的

缺乏访问控制

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

编程难度提高

        编写与调试一个多线程程序比单线程程序困难得多(这难道不是我的缺点吗)

 我们可以验证一下线程健壮性低这个猫饼:

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

// 新线程
void *threadStart(void *args)
{
    while (true)
    {
        sleep(1);
        std::cout << "new thread running..." << ",pid: " << getpid() << std::endl;

        int x = rand()%5;
        if(x == 0)
        {
            int *p = nullptr;
            *p = 100;       //野指针
        }
    }
}

int main()
{
    srand(time(nullptr));
   
    pthread_t tid1;
    pthread_create(&tid1, nullptr, threadStart, (void *)"thread-new");
    
    pthread_t tid2;
    pthread_create(&tid2, nullptr, threadStart, (void *)"thread-new");
    
    pthread_t tid3;
    pthread_create(&tid3, nullptr, threadStart, (void *)"thread-new");

    // 主线程
    while (true)
    {
        sleep(1);
        std::cout << "main thread running..." << ",pid: " << getpid() << std::endl;
    }
    return 0;
}

像搓进程脚本一样,我们可以这样搓出来线程的脚本:

while :; do ps -aL |head -1 &&ps -aL |grep 'testthread'; sleep 1; done

可以看到全挂了: 

我们线程是这样的捏: 

 关于缺乏访问控制,也可以用这段代码验证一下:

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

int gval = 100;

// 新线程
void *threadStart(void *args)
{
    while (true)
    {
        sleep(1);
        std::cout << "new thread running..." << ",pid: " << getpid() << " gval: " << gval << " &gval: " << &gval << std::endl;

        // int x = rand()%50;
        // if(x == 0)
        // {
        //     int *p = nullptr;
        //     *p = 100;       //野指针
        // }
    }
}

int main()
{
    srand(time(nullptr));

    pthread_t tid1;
    pthread_create(&tid1, nullptr, threadStart, (void *)"thread-new");

    pthread_t tid2;
    pthread_create(&tid2, nullptr, threadStart, (void *)"thread-new");

    pthread_t tid3;
    pthread_create(&tid3, nullptr, threadStart, (void *)"thread-new");

    // 主线程
    while (true)
    {
        sleep(1);
        std::cout << "main thread running..." << ",pid: " << getpid() << std::endl;
        std::cout << "new thread running..." << ",pid: " << getpid() << " gval: " << gval << " &gval: " << &gval << std::endl;
        gval++;             //修改
    }
    return 0;
}

大部分地址空间的资源,多线程都是共享的(要是有的线程干扰了别的线程就不太好了)

 线程异常:单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃 ,线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

线程用途

进程是资源分配的基本单位

线程是调度的基本单位

线程共享进程数据,但也拥有自己的一部分数据:

🍇 线程中私有的部分包括:一组寄存器(硬件上下文数据---线性可以动态运行)、栈(线程在运行的时候,会形成各种临时变量,临时变量会被每个线程保存在自己的栈区)、线程ID、errno、信号屏蔽字、调度优先级

🍇 进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

文件描述符表

每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)

当前工作目录

用户id和组id

写多线程和库有什么关系呢?

为什么多线程会涉及到库这样的概念呢?

Linux系统中没有线程,只有轻量级进程(但这不就是线程吗)

Linux系统没有单独设计PCB,系统只会给上层用户提供轻量级进程的接口

pthread库是Linux自带的原生线程库,会对轻量级进程的接口进行封装,把这些接口按照线程接口的方式交给用户

所以Linux的线程是用户级线程

而Windows中的线程是内核级线程

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值