【Linux】线程初步——线程概念以及接口认识

一、线程概念理解

1.何为线程------线程与进程的区别

进程简单说就是一个程序的执行过程,有了这个概念,就可以覆盖系统编程的大部分情景了,那么线程的出现又有什么意义呢?其实程序的一段执行过程确实可以叫做进程,但是真正的执行对象并不是进程 ,而是线程(轻量化的执行单元) 。大体来说,进程相当于资源分配的基本单位 ,而线程是调度的基本单位 ,一个进程拥有虚拟地址空间、信号资源、文件描述符表等等资源,线程只是进程一部分,是进程真正的****执行流。对于CPU而言,它并不认识进程,它只认执行流,每个线程被创建后可以执行指定代码片段,这样一来,一个进程就可以被拆分为多个执行流,既能并发处理任务,又能大幅降低上下文切换的开销,从而提升整体运行效率进程和线程的对应关系是1:n,在之前所谓的进程其实大部分指的是单线程进程。

联系前置知识:所谓的进程切换(时间片)、调度算法、调度队列等,实质都是对线程而言而不是进程。

2.线程的特点

2.1共享进程资源

线程作为进程的一部分,共享了整个进程的虚拟地址空间。信号处理方法等资源,这样的设计大大降低了线程切换时的开销(无需进行大量资源载入),这也是线程的核心设计特色。

2.2有独立的执行上下文与栈空间

作为进程真正的执行流,线程的执行遵循调度队列、时间片轮切等原则,因此需要通过上下文的保存进行线程之间的轮切,**同时也就意味着线程具有独立的PCB结构体进行执行上下文保存。**栈私有是线程独立执行函数调用的基础。

2.3同进程内线程容易通信

由于同进程内线程具有共享进程资源的特性,也就意味着线程间不存在进程间通信的最大问题:看到同一份资源,因此线程间很容易实现通信。

2.4一个线程崩溃,全部崩溃

当某一个线程发生异常的时候,会触发硬件向进程发送信号导致进程中断退出,进程退出也就意味着线程全部退出,导致所有线程崩溃。

3.线程优缺点

优点

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

缺点

• 过多线程创建可能导致频繁线程轮切,反而降低效率。
◦ ⼀个很少被外部事件阻塞的计算密集型线程往往⽆法与其它线程共享同⼀个处理器。如果计
算密集型线程的数量⽐可⽤的处理器多,那么可能会有较⼤的性能损失,这⾥的性能损失指
的是增加了额外的同步和调度开销,⽽可⽤的资源不变。
• 由于线程间共享虚拟空间,健壮性降低
◦ 编写多线程需要更全⾯更深⼊的考虑,在⼀个多线程程序⾥,因时间分配上的细微偏差或者
因共享了不该共享的变量⽽造成不良影响的可能性是很⼤的,换句话说线程之间是缺乏保护
的。
• 缺乏访问控制
◦ 进程是访问控制的基本粒度,在⼀个线程中调⽤某些OS函数会对整个进程造成影响。
• 编程难度提⾼
◦ 编写与调试⼀个多线程程序⽐单线程程序困难得多

二、相关接口及基础用法

pthread_creat


功能:创建⼀个新的线程
原型: int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
参数:
thread:返回线程ID
attr:设置线程的属性,attr为NULL表⽰使⽤默认属性
start_routine:是个函数地址,线程启动后要执⾏的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
注意:使用pthread库时需要手动链接(-lpthread)
代码示范

cpp 复制代码
#include<pthread.h>
#include<iostream>
#include<unistd.h>
#include<string>
void *handler(void *th){
    std::string name=static_cast<const char *>(th);
    while(true){
    sleep(1);
    std::cout<<"I'm "<<name<<std::endl;
    }
}

int main(){
    pthread_t thread;
    pthread_create(&thread,NULL,handler,(void*)"thread -1");
    while(true){
        std::cout<<"I'm main thread"<<std::endl;
        sleep(1);
    }
}   

代码进行编译运行后可以看到命令行按照预期进行了输出,同时进行进程资源调取可以看到只有一个进程运行,那么如何进行线程信息查看呢?

bash 复制代码
ps -aL

再次运行上次的进程后输入该指令就可以看到目标进程下出现了两个LWP不同的线程。

补充:LWP------线程的内核层实现载体

LWP是指轻量化进程,是线程的内核层实现载体。其实在Linux中并不存在所谓线程,线程严格意义上是属于进程的子单元 ,无独立的内核数据结构,完全依赖进程的资源容器,自身仅保存执行上下文。Linux 的 LWP 有独立的 task_struct(和进程同等地位),内核调度时完全把它当成「独立单元」对待 ------ 只是它和其他 LWP 共享资源而已。Linux设计之初就简化了线程与进程之间的差异,采用统一管理的方式。因此Linux中不存在所谓线程,而是LWP(轻量化进程),在内核层就叫做LWP,而在用户层通过封装抽象成了线程。并且每个线程有自己独特的LWP标识符,图中所示就是LWP标识符。

pthread_self------线程ID获取

在进程内部,glibc同时为我们提供了线程ID的获取方法:pthread_self

可以在之前代码中加上该函数进行实验

cpp 复制代码
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <string>
void *handler(void *th)
{
    std::string name = static_cast<const char *>(th);
    pthread_t ID;
    ID=pthread_self();
    std::cout<<"ID-1 : "<<ID<<std::endl;
    while (true)
    {
        sleep(1);
        std::cout << "I'm " << name << std::endl;
    }
}

int main()
{
    pthread_t thread;
    pthread_t mID;
    pthread_create(&thread, NULL, handler, (void *)"thread -1");
    mID=pthread_self();
    std::cout<<"main ID ="<<mID<<std::endl;
    while (true)
    {
        std::cout << "I'm main thread" << std::endl;
        sleep(1);
    }
}

可以发现打印出了两个很大的数字,而且跟我们刚刚通过ps -aL查询到的:LWP标识符大小完全不一样,其实通过这个pthread_self函数获取到的ID并不是LWP提供的标识符,而是pthread库给每个线程维护的进程内唯一标识符,由于每个进程都有自己独立的空间,故而该标识符的作用域是进程内而非系统全局,这个标识符其实对应的是一个进程虚拟空间上的地址,通过这个地址可以找到该线程相关信息。

pthread_exit/pthread_cancel------线程终止

这两个函数均用于线程终止,区别在于pthread_exit函数用于终结自身线程,而pthread_cancel函数可以终结其他线程。

pthread_exit

该函数没有返回值,进行调用时可以通过retval参数带出线程返回值(等同于线程执行函数return void*)注意:不要传入局部变量的地址,局部变量存储在栈上,线程结束后栈空间会被销毁

pthread_cancel

pthread_cancel(pthread_t thread) 以目标线程的 ID(pthread_t类型)为唯一入参,核心作用是向指定线程发送取消信号,触发其在取消点终止执行。

pthread_join------线程等待

在进程层面,子进程退出后若父进程未调用 `wait/waitpid` 回收,会产生僵尸进程(占用 PID、进程表项等内核资源),造成系统级资源泄漏; 线程层面虽不存在「僵尸线程」,但如果主线程先退出且未通过 `pthread_join`/`pthread_detach` 回收子线程,会导致子线程的用户态资源(如线程栈、TLS 线程局部存储)和内核态 `task_struct` 中部分私有数据无法及时释放,进而造成进程内的内存泄漏。

该函数调用时需要传入两个参数:

thread:通过pthread_self获取的进程内线程唯一标识符,传入需要进行等待回收的标识符即可。

retval:输出型参数 ,指向存储子线程返回值的地址 ,若不需要接收返回值可传 NULL

  1. 子线程通过 pthread_exit(void *ret) 或子线程执行函数 return void* 设定退出返回值;
  2. 内核会保存这个返回值,直到主线程调用 pthread_join
  3. 主线程通过 retval 拿到子线程的返回值(*retval 就是子线程的返回值)。

补充:clone------pthread底层实现

pthread 库调用pthread_create()创建线程时,底层会调用clone(),并传入核心参数:

  • CLONE_VM:新线程(LWP)与父进程共享虚拟地址空间(包括堆、全局变量),这是线程与独立进程的核心区别;
  • CLONE_FILES:共享文件描述符表,线程间可复用打开的文件、套接字等;
  • CLONE_SIGHAND:共享信号处理函数,避免重复注册信号逻辑;
  • CLONE_THREAD:将新线程加入父进程的线程组,统一管理 PID/LWP;这些参数组合让新创建的执行流成为 "共享大部分资源的轻量级进程",即用户态感知的 "线程"。

pthread库的封装通过clone()的参数控制资源共享粒度,将内核的轻量级进程(LWP)转化为用户态的 "线程" 概念,并提供统一的线程创建、同步、管理接口,既复用了 Linux 内核的进程调度逻辑,又满足了多线程开发的易用性需求。

相关推荐
cuber膜拜2 小时前
Tenacity 原理与基本使用
服务器·网络·python·装饰器模式·tenacity
cuber膜拜2 小时前
PyBreaker 原理与基本使用
服务器·网络·python·pybreaker
Albert Edison2 小时前
【Python】文件
android·服务器·python
未来之窗软件服务2 小时前
服务器运维(三十三)日志分析ssh日志工具—东方仙盟
运维·服务器·ssh·仙盟创梦ide·东方仙盟
梦雨羊2 小时前
搭建服务器进行测试
linux·运维·服务器
青主创享阁2 小时前
玄晶引擎2.7.6技术拆解+实战略落地:春节前自动化运营能力升级全解析
运维·自动化
FLS1682 小时前
华为S5700交换机SSH/Telnet/Web登录完整配置流程(V200R005C00SPC500)
运维·网络·华为·ssh
哼?~2 小时前
磁盘与文件系统
linux
Bigbig.3 小时前
Linux 挖矿病毒深度排查与修复实录
linux·安全