跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

Linux/C++多线程编程入门:核心概念与常用函数详解

Linux 环境下 C/C++ 多线程编程基础教程。解析进程与线程区别、并发与并行概念。详解 pthread 库核心函数,包括线程创建 pthread_create、获取线程号 pthread_self、退出 pthread_exit、资源回收 pthread_join 及分离态 pthread_detach。提供文件拷贝实战案例,说明多线程协作方式。提示线程同步与竞态条件风险。

观心发布于 2026/3/22更新于 2026/8/1743 浏览
Linux/C++多线程编程入门:核心概念与常用函数详解

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

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

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

  • 并发:指系统能够同时处理多个任务的能力。在单核 CPU 上,通过时间片轮转,宏观上看起来是'同时'运行,微观上是'交替'执行。
  • 并行:指多个任务在同一时刻真正同时运行。这需要多核 CPU 支持,每个核跑一个线程。

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

多线程基础概念

一、进程与线程的区别

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

二、进程与线程的关系

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

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

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

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

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

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

三、多线程的优缺点

优点
  • 响应性:即使一个线程阻塞(如等待 I/O),其他线程仍可运行。
  • 资源共享:线程间共享内存,无需复杂 IPC。
  • 经济性:创建线程比创建进程开销小得多。
  • 可扩展性:可充分利用多核 CPU。
缺点
  • CPU 调度:操作系统内核负责线程调度,上下文切换需要保存/恢复寄存器、更新内存管理结构,这些都需要时间。如果线程数远大于 CPU 核心数,切换开销会占据大量 CPU 时间。
  • 开发复杂度大大增加:多线程引入了竞态条件、死锁等并发问题,这些错误往往难以复现和调试。比如我们曾遇到一个死锁问题,只在生产环境高并发时出现,用 gdb 无法重现,最后靠分析 core dump 和代码走查才定位到。

多线程编程

我们先讲 C/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 仍需要填写

#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;
}
向线程体中传入多个数据

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

#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,失败返回一个错误码

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

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

六、多线程编程的小练习

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

#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)以及无锁编程。

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

目录

  1. 前言:为什么需要多线程?
  2. 多线程基础概念
  3. 一、进程与线程的区别
  4. 二、进程与线程的关系
  5. 三、多线程的优缺点
  6. 优点
  7. 缺点
  8. 多线程编程
  9. 一、创建线程:pthread_create
  10. 向线程体中传递单个数据
  11. 向线程体中传入多个数据
  12. 二、线程号的获取:pthread_self
  13. 三、线程的退出函数:pthread_exit
  14. 四、线程的资源回收:pthread_join
  15. 五、线程分离态:pthread_detach
  16. 小问题
  17. 方法 1:使用 pthread_exit()
  18. 方法 2:让主线程等待(但不阻塞)
  19. 六、多线程编程的小练习
  20. 结语
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • AI 大模型系统学习路线:从入门基础到工程实战
  • 解决安装 WSL 及 Ubuntu 时出现的安全频道支持错误
  • HTTP 响应状态码详解
  • Java Web 开发实战:数据库操作与会话技术
  • C++ 线程安全容器设计与 STL 标准库兼容性解析
  • CentOS 7 及多种 Linux 发行版命令行连接 Wi-Fi 方法
  • GitHub 每日热门项目榜单 (2026-02-22)
  • 算法竞赛入门经典第 5 章:C++ 与 STL 入门代码示例
  • 网络安全入门指南:学习路线与方向选择
  • 大语言模型 LoRA 技术综述:原理、变体与应用
  • 2026 无人机 AI 算法全景图:7 大场景 50+ 算法详解
  • VSCode Remote-SSH 远程服务器代理配置错误排查与修复
  • Spring Boot 安全认证与授权实战
  • IDEA 中 Module 依赖 JAR 失效及优先使用 Module 编译的处理方法
  • OpenClaw 部署指南:Minimax/DeepSeek 模型与飞书机器人配置
  • Linux 基础 IO:文件描述符、重定向与缓冲区原理
  • Python 面向对象编程核心指南
  • C++ 模板编程入门:泛型编程核心概念与实战
  • Python 列表内存存储本质:差异原因与优化建议
  • 程序员转型 AI 产品经理:核心能力与成长路径

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online