Linux 线程:从零构建多线程应用:系统化解析线程API与底层设计逻辑

发布于:2025-04-12 ⋅ 阅读:(43) ⋅ 点赞:(0)

线程

线程的概述

在之前,我们常把进程定义为 程序执行的实例,实际不然,进程实际上只是维护应用程序的各种资源,并不执行什么。真正执行具体任务的是线程。

那为什么之前直接执行a.out的时候,没有这种感受呢?

那是因为每一个进程中都会有一个主线程,我们默认执行的就是这个主线程。

线程创建比进程简单

进程通过返回值确定 是哪块进程的代码。

线程不需要,创建一个线程,比较简单,像回调函数一样,调用线程创建函数,在对应函数体中 操作这一线程即可。

从这往下的概述部分 重点(理解背诵)

进程是系统分配资源的基本单位,线程是CPU执行基本调度的基本单位

比如 如果线程是具体某个人,那么进程就是指部门

线程可以看作一个轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程

进程 必须至少包含一个线程

线程依赖于进程,线程共享进程的资源,线程的系统资源有(计数器,一组寄存器和栈)

进程结束 当前进程的所有线程 都将立即结束

Linux内核是不区分进程和线程的,只有在用户层面上进行区分。所以,进程所有操作函数pthread*是库函数,而并非系统调用

线程共享资源

  1. 文件描述符
  2. 每种信号的处理方式
  3. 当前工作目录
  4. 用户ID和组ID 内存地址空间

线程非共享资源

  1. 线程id
  2. 处理器现场和栈指针
  3. 独立的栈空间
  4. errno变量
  5. 信号屏蔽字
  6. 调度优先级

线程被CPU调度,因此线程中有调度优先级,且线程间不共享

查看指定进程的线程号的命令:ps -Lf pid(进程号)

线程的API

API介绍用的代码 较简短的代码我用图片展示。

只要看到了pthread.h 头文件,我们在编译的时候就需要加上 -lpthread

pthread_t 是无符号长整型

1、查看线程号

#include <pthread.h>

pthread_t pthread_self(void);

功能:

        查看线程号

参数:

        无参

返回值:

        调用该函数的线程 的 线程ID

代码演示

代码运行结果

线程ID(通过pthread_self得到) 和 IPW(轻量级进程)的区别

大家看这张图,可以看到这两个值有明显的区别

在Linux中,线程就是LWP(轻量级进程),全局唯一,由操作系统内核分配,用于系统调度和资源管理。

线程ID呢仅在同一进程内有效,是抽象标识符。由 pthread 库在进程内维护。

2、创建线程

#include <pthread.h>

int pthread_create(pthread_t *thread,

                                const pthread_attr_t *attr,

                                void *(*start_routine) (void *),

                                void *arg);

功能:

        创建一个线程

参数:

        thread:线程标识符地址

        attr:线程属性结构体地址,通常设置为NULL

                属性这个参数,我们现在填写NULL,下面我会详细说一下这个参数。

        start_routine:线程函数的入口地址

        arg:传递给线程函数的参数

返回值:

        成功:0

        失败:非0

代码演示 案例1

注意这里主进程一定要阻塞,因为进程结束,线程也会关闭

代码运行结果

案例二 创建进程,每个线程有自己的线程函数

代码运行结果是一样的,大家只要知道能够这样用就可以了。

3、回收线程函数

函数介绍

功能:

        等待线程结束(此函数会阻塞),并回收线程资源。如果线程已结束,那么该函数会立即返回。

参数:

        thread:被等待的进程的进程ID

        retval:用来存储线程退出状态的指针的地址

        这里细说一下:retval的返回值类型我们可以看到是void **,这个变量需要用户创建,用来存储创建函数 线程执行函数的 返回值,返回值时void*类型。由于我们要得到它,就要提前创建一个void *的变量,再通过函数修改我们创建的变量为返回值的内容,由于是函数内部要函数外部的变量的值,因此需要传递所创建void *的变量的地址,因此时void **类型。

返回值:

        成功:0

        失败:非0

代码演示

代码运行结果

注意

由于带阻塞,因此有顺序,如下面这种情况

先等待tid1结束,回收tid1后,才会回收tid2

不管谁先结束,都是先1 后2

进程分离

创建好线程后,当多个任务同时进行,用上面的方法,会阻塞线程的释放,导致资源浪费(长时间不适用却霸占内存),因此这里 我们就将其分离出去,把释放工作交给系统,系统发现它结束,就会释放

由于它的归属权已经归于系统,此时我们就不可以再对它使用join

注意这里的分离,并不是该线程不依赖于进程,而是将 释放线程独立资源 的权限交给了系统,进程还是依赖与进程的,依旧共享进程的空间

函数介绍

#include <pthread.h>

int pthread_detach(pthread_t thread);

功能:

        使调用线程的独立资源回收工作与当前进程分离

参数:

        thread:线程ID

返回值:

        成功:0

        失败:非零

代码演示 主线程和子线程

本代码将实现 主线程和子线程 一起运行,并且利用主线程的正常工作,来验证pthread_detach的不阻塞的特性

代码运行结果

4、线程的取消和退出

        注意要退出线程 一定不要调用exit或者_exit 这两个是退出进程的函数,如果调用这个在线程中知道你的进程是什么,它会将进程退出,进程退出会导致所有的线程退出,那么我们该怎么让单个线程退出呢?

1、线程的退出(自杀)

#include <pthread.h>

void pthread_exit(void *retval);

函数功能:

        退出调用线程。一个进程中的多个线程是共享该进程的数据段的,因此通常线程退出后,所占用的资源并不会释放。

参数:

        retval:存储线程退出状态的指针(return后的数据)

返回值:

        无

2、线程的取消(他杀)

取消本线程,也可以取消当前进程的其他线程

#include <pthread.h>

int pthread_cancel(pthread_t thread);

功能:

        退出调用线程。一个进程中的多个线程是共享该进程的数据段的,因此通常线程退出后,所占用的资源并不会释放。

参数:

        thread:目标线程ID

返回值:

        成功:0

        失败:出错编号

注意

        杀死线程也不是立刻就能完成,必须要到达取消点

        取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用

代码演示:

代码功能:子线程1实现5s后自杀,子线程2在7s时杀死子线程3,子线程2在10s时杀死自己。

这里我们会与遇到一个问题:当我们在线程2 中,我们首先需要传入本线程的名字(线程2),还需要传入子线程3的线程ID,我们该如何实现传两个参数呢?

答案在代码中,大家自己查看。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

//传递两个参数的办法就是借助结构体,将线程名和ID作为结构体成员后,将结构体传入 线程调用函数 中即可
//并且如果要实现tid3的修改同步到结构体内,需要传递tid3的指针类型
typedef struct dataDouble
{
    char name[32];
    pthread_t *id;
}DATA;


//线程调用函数声明
void *my_fun1(void *arg);
void *my_fun2(void *arg);
void *my_fun3(void *arg);
int main(int argc, char const *argv[])
{
    //创建线程ID遍历(存放线程ID)
    pthread_t tid1,tid2,tid3;

    DATA *tid2_data = (DATA *)calloc(1,sizeof(DATA));
    tid2_data->id = &tid3;
    strcpy(tid2_data->name,"子进程2");

    //创建线程
    pthread_create(&tid1,NULL,my_fun1,(void *)"子线程1");
    pthread_create(&tid2,NULL,my_fun2,(void *)tid2_data);
    pthread_create(&tid3,NULL,my_fun3,(void *)"子线程3");

    //释放线程
    pthread_detach(tid1);
    pthread_detach(tid2);
    pthread_detach(tid3);

    //阻塞进程
    while(1);

    //释放结构体申请空间,一定要在全部线程结束之后
    free(tid2_data);
    return 0;
}
//线程调用函数体实现
void *my_fun1(void *arg)//线程1 在5s的时候自杀
{
    int i = 0;
    while(1)
    {
        sleep(1);
        printf("----%s的运行时间为:%d\n",(char *)arg,++i);
        if(i == 5)
        {
            pthread_exit(NULL);
        }
    }
}
void *my_fun2(void *arg)//线程2 在7s的时候杀死线程3,在10s的时候自杀(使用cancel)
{
    DATA data = *(DATA *)arg;
    int i = 0;
    while(1)
    {
        sleep(1);
        printf("--------%s的运行时间为:%d\n",data.name,++i);
        if(i == 7)
        {
            pthread_cancel(*data.id);
        }
        if(i == 10)
        {
            pthread_cancel(pthread_self());
        }
    }
}
void *my_fun3(void *arg)
{
    int i = 0;
    while(1)
    {
        sleep(1);
        printf("------------%s的运行时间为:%d\n",(char *)arg,++i);
    }
}

代码运行结果

结束

代码重在练习!

代码重在练习!

代码重在练习!

今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏夹关注,谢谢大家!!!

下篇介绍:线程的属性介绍,线程池的简述,多线程的建立


网站公告

今日签到

点亮在社区的每一天
去签到