【Linux/C++多线程篇(一) 】多线程编程入门:从核心概念到常用函数详解

⭐️在这个怀疑的年代,我们依然需要信仰

个人主页:YYYing.

⭐️Linux/C++进阶 系列专栏:【从零开始的linux/c++进阶编程】

系列上期内容:【Linux/C++多进程篇(二) 】linux系统编程之进程间通信 (IPC)

系列下期内容:暂无


目录

前言:为什么需要多线程?

多线程基础概念

一、进程与线程的区别

二、进程与线程的关系

三、多线程的优缺点

[📖 优点](#📖 优点)

[📖 缺点](#📖 缺点)

多线程编程

一、创建线程:pthread_create

[📖 向线程体中传递单个数据](#📖 向线程体中传递单个数据)

[📖 向线程体中传入多个数据](#📖 向线程体中传入多个数据)

二、线程号的获取:pthread_self

三、线程的退出函数:pthread_exit

四、线程的资源回收:pthread_join

五、线程分离态:pthread_detach

[📖 小问题](#📖 小问题)

[方法1:使用 pthread_exit()](#方法1:使用 pthread_exit())

方法2:让主线程等待(但不阻塞)

六、多线程编程的小练习

结语

---⭐️封面自取⭐️---



前言:为什么需要多线程?

想象一下,你现在是一个厨师,需要同时做三件事:切菜、煮汤、接电话。如果你只能一件一件地做,顾客会等得不耐烦,汤可能煮干,电话也可能被挂断。在计算机世界里,程序也常常需要同时处理多个任务------比如一边响应用户操作,一边下载文件,一边更新界面。其也是为了实现多任务并发执行的问题的,能够实现多个阻塞任务同时执行

多线程就是让一个程序拥有多个执行流,可以"同时"做多件事。这里的"同时"有两种含义:

  • 并发:指系统能够同时处理多个任务的能力。在单核 CPU 上,通过时间片轮转,宏观上看起来是"同时"运行,微观上是"交替"执行。

  • 并行 :指多个任务在同一时刻真正同时运行。这需要多核 CPU 支持,每个核跑一个线程。

多线程的核心价值在于提高程序的响应性、资源利用率和吞吐量

多线程基础概念

一、进程与线程的区别

  • 进程:是资源分配的基本单位,拥有独立的地址空间、文件描述符、堆栈等。进程间切换开销大。

  • 线程:是CPU调度的基本单位,隶属于进程,共享进程的地址空间和大部分资源,拥有独立的栈和寄存器上下文。线程间切换开销小,通信方便。


二、进程与线程的关系

  • 多线程(LWP轻量版进程):线程是粒度更小的任务执行单元

  • 进程是资源分配的基本单位,而线程是任务器进行任务调度的最小单位

  • 一个进程可以拥有多个线程,同一个进程中的多个线程共享进程的资源,而 由于线程是共用进程的资源,所以对于线程的切换而言,开销较小,但多个线程使用的是同一个进程的资源 ,那么就会导致每个进程使用资源时,产生资源抢占问题,没有多进程安全

  • 每个进程至少有一个线程:主线程

  • 只要有一个线程中退出了进程,那么所有的线程也就结束了,主线程结束后,整个进程也就结束了。

  • 多个线程执行顺序:没有先后顺序,按时间片轮询,上下文切换,抢占CPU的方式进行


三、多线程的优缺点

📖 优点

  • 响应性:即使一个线程阻塞(如等待I/O),其他线程仍可运行。

  • 资源共享:线程间共享内存,无需复杂IPC。

  • 经济性:创建线程比创建进程开销小得多。

  • 可扩展性:可充分利用多核CPU。

📖 缺点

  • CPU 调度:操作系统内核负责线程调度,上下文切换需要保存/恢复寄存器、更新内存管理结构,这些都需要时间。如果线程数远大于 CPU 核心数,切换开销会占据大量 CPU 时间。

  • **开发复杂度大大增加:**多线程引入了竞态条件、死锁等并发问题,这些错误往往难以复现和调试。比如我们曾遇到一个死锁问题,只在生产环境高并发时出现,用 gdb 无法重现,最后靠分析 core dump 和代码走查才定位到。


多线程编程

我们先讲C语言的多线程编程,至于C++的线程支持库我们后面也会讲。

再讲之前多提一嘴,由于C库没有提供有关多线程的相关操作,对于多线程编程要依赖于第三方库------头文件:#include<pthread.h>,且编译时:需要加上 -lpthread 选项,链接上对于的线程支持库

一、创建线程:pthread_create

|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 函数原型 | int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine) (void *), void *arg); |
| 头文件 | pthread.h |
| 功能 | 创建一个分支线程 |
| 参数说明 | 参数1:线程号,通过参数返回,用法:在外部定义一个该类型的变量,将地址传递入函数,调用结束后,该变量中即是线程号 参数2:线程属性,一般填NULL,让系统使用默认属性创建一个线程 参数3:是一个回调函数,一个函数指针,需要向该参数中传递一个函数名,作为线程体执行函数该函数由用户自己定义,参数是void*类型,返回值也是void *类型 参数4: 是参数3的参数,如果不想向线程体内传递数据,填NULL即可 |
| 返回值 | 成功返回0,失败返回一个错误码(非linux内核的错误码,是线程支持库中定义的一个错误码) |

我们现在来实践一下此处的创建线程函数,其中我们线程体传递参数不同数目写法也不一样,现在我们来看看:

📖 向线程体中传递单个数据

**注意:我们传过来的参数必须要进行强转,**当然此处若不想添加参数,就直接填NULL即可,但线程体中的 void* arg 仍需要填写

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include <unistd.h>
#include <pthread.h>
using namespace std;

//定义线程体函数
void *task(void *arg){
    //arg --> &num  但是arg是一个void*类型的变量,需要转换为具体指针进行操作
    //(int*)arg    --->将其转换为整型的指针
    //*(int *)arg  --->num的值
    int key = *(int*)arg;
    printf("我是分支线程:num = %d\n", key);    //1314
}

/**************主程序********************************/
int main(int argc, const char *argv[]){
    pthread_t tid = -1;
    //用于存储线程号的变量
    int num = 520;
    if(pthread_create(&tid, NULL, task, &num) != 0){
    //参数2:表示让系统使用默认属性创建一个线程
    //参数3:线程体函数名
    //参数4:表示向线程体中传递的数据
        printf("tid create error\n");
        return -1;
    }
    printf("pthread_create success,tid = %#lx\n", tid);
    printf("我是主线程\n");
    num = 1314;
    //主线程中更改数据
    printf("主线程中num = %d\n", num);

    while(1);

    return 0;
}

📖 向线程体中传入多个数据

可以看到,此处我们若想传递多个数据就必须用到结构体,不能用逗号隔开一个一个传

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include <unistd.h>
#include <pthread.h>
using namespace std;

//定义信息结构体,用于向线程体传递数据
struct Info{
    int num;
    char name[20];
    double score;
};

//定义线程体函数
void *task(void *arg){
    Info buf = *((Info*)arg);         
    //将结构体指针转换为结构体变量
    printf("分支线程中:num = %d, name = %s, score = %.2lf\n", buf.num, buf.name, buf.score);
}

/**************主程序********************************/
int main(int argc, const char *argv[])
{
    pthread_t tid = -1;
    //用于存储线程号的变量
    int num = 520;
    char name[20] = "zhangsan";
    double score = 99.5;
    //需求:将上面的三个数据全部传入线程体中
    Info buf = {num, "zhangsan", score};
    if(pthread_create(&tid, NULL, task, &buf) != 0){
    //参数2:表示让系统使用默认属性创建一个线程
    //参数3:线程体函数名
    //参数4:表示向线程体中传递的数据
        printf("tid create error\n");
        return -1;
    }
    printf("pthread_create success,tid = %#lx\n", tid);
    printf("我是主线程\n");
    while(1);
    return 0;
}

二、线程号的获取:pthread_self

|----------|-----------------------------------|
| 函数原型 | pthread_t pthread_self(void); |
| 头文件 | pthread.h |
| 功能 | 获取当前线程的线程号 |
| 参数说明 | |
| 返回值 | 返回调用线程的id号,不会失败 |


三、线程的退出函数:pthread_exit

|----------|---------------------------------------|
| 函数原型 | void pthread_exit(void *retval); |
| 头文件 | pthread.h |
| 功能 | 退出当前线程 |
| 参数说明 | 表示退出时的状态,一般填NULL |
| 返回值 | |


四、线程的资源回收:pthread_join

|----------|----------------------------------------------------------|
| 函数原型 | int pthread_join(pthread_t thread, void **retval); |
| 头文件 | pthread.h |
| 功能 | 阻塞回收指定线程的资源 |
| 参数说明 | 参数1:要回收的线程线程号 参数2:线程退出时的状态,一般填NULL |
| 返回值 | 成功返回0,失败返回一个错误码 |


五、线程分离态:pthread_detach

|----------|-------------------------------------------|
| 函数原型 | int pthread_detach(pthread_t thread); |
| 头文件 | pthread.h |
| 功能 | 将指定线程设置成分离态,被设置成分离态的线程,退出后,资源由系统自动回收 |
| 参数说明 | 要分离的线程号 |
| 返回值 | 成功返回0,失败返回一个错误码 |

我们不妨更直观的看看四和五的区别:

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

//定义线程体函数
void *task(void *arg)
{
    printf("分支线程,tid = %#x\n", pthread_self());     //2、调用函数,输出当前线程的线程号
    sleep(3);
    //3、退出线程
    pthread_exit(NULL);
}

/**********************主程序****************************/
int main() {
    //1、定义一个线程号变量
    pthread_t tid = -1;
    //创建一个线程
    if(pthread_create(&tid, NULL, task, NULL) != 0)
    {
        printf("pthread_create error\n");
        return -1;
    }

    printf("主线程,tid = %#x\n", tid);
    //4、回收分支线程的资源
    //pthread_join(tid, NULL);        //前3秒,主线程处于休眠等待分支线程的结束
                                    //分支线程处于休眠状态
    
    //将线程设置成分离态(非阻塞)
    pthread_detach(tid);
    sleep(5);

    // while(1);                      
    //防止主线程结束
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

📖 小问题

那我相信肯定也会有人发现,那分离进程如果没运行完,主程序退出后,那么此时分离进程会被回收吗?

答案是:会的,但是线程不会有机会执行自己的清理代码。也就是不会自动"回收分离进程",而是整个进程会被强制终止,所有线程(包括分离线程)都会被立即杀死。

  • 主线程退出(从main返回或调用exit()) -> 整个进程终止,所有线程(包括分离线程)立即停止,资源由操作系统回收,但不会执行线程的清理代码。

  • 主线程调用pthread_exit() -> 主线程终止,但进程继续运行,直到所有线程(包括分离线程)结束。这样分离线程可以正常完成并执行清理代码。

所以面对这种情况,我们有两种解决方式:

方法1:使用 pthread_exit()
cpp 复制代码
int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, worker, NULL);
    pthread_detach(tid);
    
    // 只退出主线程,不终止进程
    pthread_exit(NULL);  // 进程继续,分离线程可以运行完成
    
    // 这里不会执行
    return 0;
}
方法2:让主线程等待(但不阻塞)
cpp 复制代码
int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, worker, NULL);
    pthread_detach(tid);
    
    // 主线程做自己的事,但最后等待一下
    // 比如用条件变量或简单的sleep
    sleep(10);  // 等待worker线程大概完成
    
    return 0;
}

六、多线程编程的小练习

我们现在使用多线程完成两个文件的拷贝,线程1拷贝前一半内容,线程2拷贝后一半内容,主线程用于回收两个分支线程的资源

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include<pthread.h>

//定义要向线程体函数中出入数据的结构体类型
struct Info
{
    const char *srcfile;    //要拷贝的原文件
    const char *destfile;   //目标文件
    int start;              //起始位置
    int len;                //要拷贝的长度
};

//定义获取文件长度的函数
int get_file_len(const char *srcfile, const char *destfile)
{
    //定义两个文件描述符,分别作为源文件和目标文件的句柄
    int sfd, dfd;
    //以只读的形式打开源文件
    if((sfd = open(srcfile, O_RDONLY)) == -1)
    {
        perror("open srcfile error");
        return -1;
    }

    //以只写的形式打开目标文件
    if((dfd = open(destfile, O_RDWR|O_CREAT|O_TRUNC, 0664)) == -1)
    {
        perror("open destfile error");
        return -1;
    }

    //获取源文件的长度
    int len = lseek(sfd, 0, SEEK_END);

    //关闭文件
    close(sfd);
    close(dfd);
    return len;
}

//定义线程体函数
void *task(void *arg)
{
    //将传入的数据解析出来
    const char *srcfile = ((struct Info*)arg)->srcfile;
    const char *destfile = ((struct Info*)arg)->destfile;
    int start = ((struct Info*)arg)->start;
    int len = ((struct Info*)arg)->len;
    //准备拷贝工作
    //定义两个文件描述符,分别作为源文件和目标文件的句柄
    int sfd, dfd;
    //以只读的形式打开源文件
    if((sfd = open(srcfile, O_RDONLY)) == -1)
    {
        perror("open srcfile error");
        return NULL;
    }

    //以只写的形式打开目标文件
    if((dfd = open(destfile, O_RDWR)) == -1)
    {
        perror("open destfile error");
        return NULL;
    }

    //偏移指针
    lseek(sfd, start, SEEK_SET);
    lseek(dfd, start, SEEK_SET);

    //拷贝工作
    int ret = 0;         //记录每次读取的数据
    int count = 0;        //记录拷贝的总个数
    char buf[128] = "";         //数据搬运工
    while(1)
    {
        ret = read(sfd, buf, sizeof(buf));
        //将读取的数据放入到count中
        count += ret;
        if(count >= len)
        {
            //说明该部分的内容拷贝结束,还剩最后一次
            write(dfd, buf, ret - (count-len));
            break;
        }

        //其余的正常拷贝
        write(dfd, buf, ret);
    }
    //关闭文件描述符
    close(dfd);
    close(sfd);
}

int main(int argc, const char *argv[]) 
{
    //判断传入的文件个数是否正确
    if(argc != 3)
    {
        printf("input file error\n");
        printf("usage:./a.out srcfile destfile\n");
        return -1;
    }

    //获取原文件的长度,顺便将目标文件创建出来
    int len = get_file_len(argv[1], argv[2]);

    //创建两个线程
    pthread_t tid1, tid2;

    //定义向线程体函数传参的变量
    struct Info buf[2] = {{argv[1], argv[2], 0, len/2}, \
                            {argv[1], argv[2], len/2, len-len/2}};
                            
    if(pthread_create(&tid1, NULL, task, &buf[0]) != 0)
    {
        printf("线程创建失败\n");
        return -1;
    }

    if(pthread_create(&tid2, NULL, task, &buf[1]) != 0)
    {
        printf("线程创建失败\n");
        return -1;
    }

    //主线程中完成对两个分支线程资源的回收
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    printf("拷贝成功\n");

    return 0;
}

结语

文介绍了多线程的基础概念和各种函数的基本用法。但这只是冰山一角,真正的挑战在于后续的线程同步 (Mutex, Condition Variable)、原子操作 (Atomic)以及无锁编程

在下一篇博客中,我们将深入探讨如何解决竞态条件,揭开互斥锁与同步机制的神秘面纱。

我是YYYing, 后面还有更精彩的内容,希望各位能多多关注支持一下主包。

无限进步,我们下次再见!


---⭐️ 封面自取 ⭐️---

相关推荐
badhope1 小时前
OpenClaw卸载命令全解析
java·linux·人工智能·python·sql·数据挖掘·策略模式
一起搞IT吧1 小时前
Android功耗系列专题理论之十六:功耗不同阶段&不同模块分析说明
android·c++·智能手机·性能优化
炸膛坦客1 小时前
单片机/C语言八股:(十三)C 语言实现矩阵乘法
c语言·开发语言·矩阵
黄昏晓x1 小时前
Linux----进程通信
linux·运维·服务器
暴力求解1 小时前
Linux---动静态库的制作和使用
linux·运维·服务器
见青..2 小时前
攻防世界-web:php2、easyupload
笔记·安全·题解
荣光属于凯撒2 小时前
P15755 [JAG 2025 Summer Camp #1] JAG Box
c++·算法·贪心算法
摇滚侠2 小时前
虚拟机部署龙虾 OpenClaw,VMware 安装 Linux CentOS 虚拟机操作系统,部署 Docker,部署 OpenClaw
linux·docker·centos
EnCi Zheng2 小时前
L1C-VMware创建CentOS虚拟机完全指南 [特殊字符]
linux·运维·centos