初步了解Linux中的线程概率及线程控制

线程

概念

既然说到了线程,我们首先应该知道什么叫做线程,我们已经知道了进程有我们的PCB+数据和代码,还有进程地址空间等,而线程 我们可以看作是在一个进程中除了PCB共用 其他数据的一个进程,也就是说我们的进程拥有独立的PCB,除此之外其他的都是用的进程的。如下图所示,我们拥有的每一个线程都有一个task_struct结构体,但是他们共用了一个进程地址空间,一个页表等。所以我们把线程也称作轻量级进程。 所以我们之前学到的进程,可以看作是只有一个PCB的线程。所以我们重新定义进程和线程之间的概率。进程:是操作系统承担分配的资源的基本实体,线程:是操作系统调度的基本单位,也叫做一个执行流。

之所以叫做轻量化进程除了共用了地址空间等数据还有别的原因,在我们的CPU运行的时候,是不需要区别当前执行的是进程还是线程的,因为他们都是利用task_struct结构来进行排队,当我们切换进程的时候,PCB中保存的有我们当前进程的上下文需要加载到我们CPU的寄存器中,而在寄存器和内存之间还有一个cache,这里面存放的是当前进程进程需要用到的"热数据",所以切换进程时,cache中的数据也需要被替换,但是当我们切换线程的时候,我们用的数据是一样的,所以就不需要进行cache中的数据替换,大大加快了程序的效率,这也是轻量的重要原因。

总的来说:

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

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

线程的缺点

  • 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多

线程异常

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

线程用途

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

知识补充:页表

在我们的进程中,我们的虚拟地址是由我们的页表所转化的,页表中存在我们的虚拟地址,物理地址和相关权限信息,但是我们的在x86的架构下,如果对4G的地址空间进行转化,每一个地址都对应一个页表项,那我们的页表都巨大无比,所以我们的页表是如下设计的:

页表将我们32位的虚拟地址分为三部分,10位,10位,12位,并且页表由页目录和二级页表组成,他们都是指针数组,用于存放地址。前10位是页目录的下标,里面存放的是二级页表的地址,中间10位,是我们的二级页表的下标,存放的是页框的起始地址(物理内存以页框(4kb)为大小进行分块),最后的12位刚好是4kb,对应的是在页框中的偏移量,这样我们就可以通过一个虚拟地址的前十位在页目录中找到我们的二级页表,再由中间10位找到我们对应的页框,再由偏移量知道我们的准确地址。而大小确实小了很多。

线程vs进程

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

线程是调度的基本单位

线程共享进程数据,但也拥有自己的一部分数据:线程ID、一组寄存器、errno 、信号屏蔽字 、调度优先级

进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程 中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境: 文件描述符表每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数) 、当前工作目录 、用户id和组id

进程和线程的关系如下图:

线程控制

在我们的Linux内核中,是没有进程的概念的,只有我们的轻量级进程,所以在我们的Linux中,是没有我们所对应的线程的系统调用接口,但是我们的用户群体为了明显的区分进程和线程的使用,所以Linux程序员就自己在应用层给我们的轻量级进程进行了封装,形成了我们现在所使用的pthread库,他是为用户提供线程接口的库,在Linux中编写多线程代码需要用到pthread库,并且几乎所有的Linux系统都会自带我们的篇thread库,所以与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"打头的 ,要使用这些函数库,要通过引入头文<pthread.h> ,链接这些线程函数库时要使用编译器命令的"-lpthread"选项

线程控制接口

pthread_create

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,

void *(*start_routine) (void *), void *arg);

Compile and link with -pthread.

功能:创建一个新线程

参数:

  • thread:返回线程ID(pthead_t 类型是pthread库中的类型,本质是一个无符号长整形)
  • attr:设置线程的属性,attr为NULL表示使用默认属性
  • start_routine:是个函数地址,线程启动后要执行的函数
  • arg:传给线程启动函数的参数

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

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小

示例:

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

using namespace std;

void *Fun(void *args)
{

    while (true)
    {
        cout << "我是被创建的线程" << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;

    pthread_create(&tid, nullptr, Fun, nullptr);
    while (true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }
    return 0;
}

运行结果:

并且我们利用指令:ps -aL 可以查看到我们当前所运行的线程,两个mythread的pid相同,但是LWP不同,LWP也就是我们的轻量级进程(light weight pid),我们的确创建出了两个进程

pthread_t 类型介绍和pthread_self

  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID 不是一回事。
  • 前面讲的线程LWP属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID

pthread_t类型本质上就是一个地址,在我们pthread动态库中,当我们每创建一个新的进程,动态库就会为我们开辟一块属于当前进程的task_struct结构体,并将这些结构体管理起来,tid就是这个task_struct结构体的起始地址。

#include <pthread.h>

pthread_t pthread_self(void);

功能:获取所在线程的tid

代码示例:

cpp 复制代码
void *Fun(void *args)
{

    while (true)
    {
           sleep(1);
        cout << "我是被创建的线程" << endl;
        printf("我对tid为:%p\n",pthread_self());
       
    }
    return nullptr;
}

运行结果:

pthread_join(线程等待)

为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。类似于僵尸进程
  • 创建新的线程不会复用刚才退出线程的地址空间。

#include <pthread.h>

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

功能:等待线程结束
参数

  • thread:线程ID
  • value_ptr:它指向一个指针,后者指向线程的返回值
  • 返回值:成功返回0;失败返回错误码

代码演示:

cpp 复制代码
void *Fun(void *args)
{
    int cnt = 0;

    while (true)
    {
           sleep(1);
        cout << "我是被创建的线程" << endl;
        printf("我对tid为:%p\n",pthread_self());
        if(cnt++==5)
        break;
       
    }
    return (void*)1;
}

int main()
{
    pthread_t tid;

    pthread_create(&tid, nullptr, Fun, nullptr);
  
   void*retval;

    pthread_join(tid,&retval);

    cout<<"线程退出的返回值为:"<<(long long int)retval<<endl;//因为在64为机下指针为8字节
    return 0;
}

运行结果:

pthread_exit

#include <pthread.h>

void pthread_exit(void *retval);

功能:线程终止
参数

  • retval:需要返回的地址或者数值,但是不要返回一个局部变量的地址,该值会传入pthread_join的retval中
  • 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

代码示例:

cpp 复制代码
void *Fun(void *args)
{
    int cnt = 0;

    while (true)
    {
           sleep(1);
        cout << "我是被创建的线程" << endl;
        printf("我对tid为:%p\n",pthread_self());
        if(cnt++==5)
        break;
       
    }

    pthread_exit((void*)11);
    // return (void*)1;
}

运行结果:

pthread_cancel

#include <pthread.h>

int pthread_cancel(pthread_t thread);

功能:取消一个执行中的线程
参数

  • thread:线程ID
  • 返回值:成功返回0;失败返回错误码

代码示例:

cpp 复制代码
void *Fun(void *args)
{
    int cnt = 0;

    while (true)
    {
        sleep(1);
        cout << "我是被创建的线程" << endl;
        printf("我对tid为:%p\n", pthread_self());
        if (cnt++ == 5)
        {
            cout << "我取消了自己" << endl;
            pthread_cancel(pthread_self());
        }
    }

    // pthread_exit((void*)11);
    // return (void*)1;
}

运行结果:由于是异常结束的线程我们无法获取错误码


thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过**return返回,**value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用**pthread_ cancel异常终掉,**value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参 数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

pthread_detch(分离线程)

  • 默认情况下,新创建的线程是需要被等待的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
  • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

#include <pthread.h>

int pthread_detach(pthread_t thread);

代码示例:

cpp 复制代码
void *thread_run(void *arg)
{
    pthread_detach(pthread_self());
    printf("%s\n", (char *)arg);
    return NULL;
}
int main(void)
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, thread_run, (void*)"thread1 run...") != 0)
    {
        printf("create thread error\n");
        return 1;
    }
    int ret = 0;
    sleep(1); // 很重要,要让线程先分离,再等待
    if (pthread_join(tid, NULL) == 0)//由于我们创建的线程已经自我分离了,所以我们是无法等待的。
    {
        printf("pthread wait success\n");
        ret = 0;
    }
    else
    {
        printf("pthread wait failed\n");
        ret = 1;
    }
    return ret;
}

运行结果:

相关推荐
白日梦想家6812 小时前
第三篇:Node.js 性能优化实战:提升服务并发与稳定性
linux·编辑器·vim
i建模2 小时前
在 Ubuntu 中为 npm 切换国内镜像源
linux·ubuntu·npm
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [drivers][gpio]gpio
linux·笔记·学习
Art&Code2 小时前
M系列Mac保姆级教程:Clawdbot安装+API配置,30分钟解锁AI自动化!
运维·macos·自动化
玉梅小洋2 小时前
GitHub SSH配置教程
运维·ssh·github
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 基于python网络安全知识在线答题系统为例,包含答辩的问题和答案
开发语言·python·web安全
wjs20242 小时前
PHP Misc
开发语言
Highcharts.js2 小时前
Next.js 集成 Highcharts 官网文档说明(2025 新版)
开发语言·前端·javascript·react.js·开发文档·next.js·highcharts
CodeByV2 小时前
【Qt】信号与槽
开发语言·qt