Linux —— 线程的概念

目录

[1. Linux线程的概念](#1. Linux线程的概念)

[1.1 什么是线程?](#1.1 什么是线程?)

[1.2 页表的补充知识](#1.2 页表的补充知识)

[1.2.1 正确的页表映射关系和页表结构:](#1.2.1 正确的页表映射关系和页表结构:)

[1.3. 理解资源划分的本质,再来谈什么叫做线程](#1.3. 理解资源划分的本质,再来谈什么叫做线程)

[1.3.1 直接写一个多线程的代码](#1.3.1 直接写一个多线程的代码)

[1.3.2 从可执行程序角度,理解资源 "划分"](#1.3.2 从可执行程序角度,理解资源 "划分")

[1.3.3 Linux中多线程的实现 --- 内核角度](#1.3.3 Linux中多线程的实现 --- 内核角度)

[1.3.4 验证](#1.3.4 验证)

​编辑

[1.3.5 线程的优点](#1.3.5 线程的优点)

[1.3.6 线程缺点](#1.3.6 线程缺点)

[1.3.7 线程的异常](#1.3.7 线程的异常)

[1.3.8 线程的用途](#1.3.8 线程的用途)


1. Linux线程的概念

1.1 什么是线程?

  • 在⼀个程序⾥的⼀个执行路线就叫做线程(thread)。更准确的定义是:线程是"⼀个进程内部的控制序列"
  • ⼀切进程⾄少都有⼀个执⾏线程
  • 线程在进程内部运⾏,本质是在进程地址空间内运⾏
  • 在Linux系统中,在CPU眼中,看到的PCB都要⽐传统的进程更加轻量化
  • 透过进程虚拟地址空间,通过页表,可以看到进程的⼤部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

1.2 页表的补充知识

页表构建映射关系,是按照字节去映射的吗??------ 不是的,为什么???

1.2.1 正确的页表映射关系和页表结构:

内核用 struct page 结构表⽰系统中的每个物理页。

单级页表对连续内存要求高,于是引⼊了多级页表,但是多级页表也是⼀把双刃剑,在减少连续存储要求且减少存储空间的同时降低了查询效率。

计算机科学中的所有问题,都可以通过添加⼀个中间层来解决。 MMU 引入了新武器,江湖⼈称快表的 TLB (其实,就是缓存,Translation Lookaside Buffer,学名转译后备缓冲器)

当 CPU 给 MMU 传新虚拟地址之后, MMU 先去问 TLB 那边有没有,如果有就直接拿到物理地址发到总线给内存,齐活。但 TLB 容量⽐较⼩,难免发生Cache Miss ,这时候 MMU 还有保底的⽼武器

⻚表,在⻚表中找到之后 MMU 除了把地址发到总线传给内存,还把这条映射关系给到TLB,让它记录⼀下刷新缓存。

1.3. 理解资源划分的本质,再来谈什么叫做线程

1.3.1 直接写一个多线程的代码

创建多线程的需要用到的接口:pthread_create

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>

void *thread_routine(void *args)
{
    std::string name = (const char*)(args);
    //第二个死循环
    while(true)
    {
        std::cout << "我是新线程...,名字:" << name << std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_routine, (void*) "thread-1");
    
    //第一个死循环
    while(true)
    {
        std::cout << "我是主线程..." << std::endl;
        sleep(1);
    }

}

运行结果:

1.3.2 从可执行程序角度,理解资源 "划分"

cpp 复制代码
void *thread_routine(void *args)
{
    std::string name = (const char *)(args);
    // 第二个死循环
    while (true)
    {
        std::cout << "我是新线程...,名字:" << name << std::endl;
        sleep(1);
    }
}

void fun()
{
    int a = 10;
    int b = 20;
    printf("a+b=%d\n", a + b);
}

int main()
{
    fun();
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");

    // 第一个死循环
    while (true)
    {
        std::cout << "我是主线程..." << std::endl;
        sleep(1);
    }
}

用 fun() 函数做演示:

1.3.3 Linux中多线程的实现 --- 内核角度

1.3.4 验证

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>

void *thread_routine(void *args)
{
    std::string name = (const char *)(args);
    // 第二个死循环
    while (true)
    {
        std::cout << "我是新线程...,名字:" << name << std::endl;
        sleep(1);
    }
}

// void fun()
// {
//     int a = 10;
//     int b = 20;
//     printf("a+b=%d\n", a + b);
// }

int main()
{
    // fun();
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");

    // 第一个死循环
    while (true)
    {
        std::cout << "我是主线程..." << std::endl;
        sleep(1);
    }
}

运行结果:

上面的运行结果中,两个执行流的PID相同,证明上面的两个执行流的属于同一个进程,对应的终端TTY都是pts/0,向同一个显示器打印,LWP:light wight process 轻量级进程

所以:

OS选择进程调度的时候(当然看的是优先级),看的是lwp !!(逻辑上)。

证明是同一个进程:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>

void *thread_routine(void *args)
{
    std::string name = (const char *)(args);
    // 第二个死循环
    while (true)
    {
        std::cout << "我是新线程...,名字:" << name << "pid: " << getpid() << std::endl;
        sleep(1);
    }
}

int main()
{
    // fun();
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");

    // 第一个死循环
    while (true)
    {
        std::cout << "我是主线程..., pid:  " << getpid() << std::endl;
        sleep(1);
    }
}

运行结果:

1.3.5 线程的优点

  • 创建⼀个新线程的代价要比创建⼀个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的⼯作要少很多
  1. 最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下⽂切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
  2. 另外⼀个隐藏的损耗是上下⽂的切换会扰乱处理器的缓存机制。简单的说,⼀旦去切换上下⽂,处理器中所有已经缓存的内存地址⼀瞬间都作废了。还有⼀个显著的区别是当你改变虚拟内存空间的时候,处理的⻚表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在⼀段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。
  • 线程占⽤的资源要比进程少
  • 能充分利⽤多处理器的可并⾏数量
  • 在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。(计算密集型:常见的算法,加密的,解密的)
  • I/O密集型应用,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。(I/O密集型:读取文件、读取网络、读取键盘)

1.3.6 线程缺点

  • 性能损失(不合理使用多线程,会导致性能损失)

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

  • 健壮性降低

编写多线程需要更全⾯更深⼊的考虑,在⼀个多线程程序⾥,因时间分配上的细微偏差或者因共享了不该共享的变量⽽造成不良影响的可能性是很⼤的,换句话说线程之间是缺乏保护的。信号发送的主体是进程。

  • 缺乏访问控制

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

  • 编程难度提高

编写与调试⼀个多线程程序⽐单线程程序困难得多。因为在这个线程中出的错误有可能不是自身的问题,有可能别的线程乱指乱写的问题

1.3.7 线程的异常

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

1.3.8 线程的用途

  • 合理的使用多线程,能提高CPU密集型程序的执⾏效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们⼀边写代码⼀边下载开发⼯具,就是多线程运行的⼀种表现)
相关推荐
Jason_chen1 小时前
Linux 6.2 音频机制深度解析:AI驱动的低延迟音频与零信任音频安全架构
linux
下午写HelloWorld1 小时前
Linux系统及Ubuntu常用指令
linux·ubuntu·操作系统
weixin_523185322 小时前
Collections.unmodifiableMap详解:真的不可修改吗?
java·linux·前端
凡人叶枫3 小时前
Effective C++ 条款04:确定对象被使用前已先被初始化
java·linux·开发语言·c++·嵌入式开发
云栖梦泽3 小时前
玩转RK3506SDK
linux·嵌入式硬件
Java面试题总结4 小时前
Linux-Ubantu-贴士-apt的地盘
linux·运维·服务器
kong@react4 小时前
Rocky Linux 10.2 全面解析:企业级 CentOS 替代方案及保姆级docker安装
java·linux·运维·docker
凡人叶枫5 小时前
Effective C++ 条款07:为多态基类声明 virtual 析构函数
linux·c语言·开发语言·c++
凡人叶枫5 小时前
Effective C++ 条款10:令 operator= 返回一个 reference to *this
java·linux·服务器·开发语言·c++·effective c++
|_⊙5 小时前
Linux 中断
linux