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

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

Linux 环境下 C++ 多线程编程基础教程。涵盖进程与线程区别、并发与并行概念、pthread 库核心函数如 create、join、detach 的使用及参数传递方式。通过文件拷贝实战演示多线程资源管理与同步机制,解析线程生命周期与资源回收策略,为后续互斥锁与同步机制学习奠定基础。

KernelLab发布于 2026/3/30更新于 2026/6/216 浏览
Linux/C++多线程编程入门:核心概念与常用函数详解

Linux/C++多线程编程入门

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

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

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

  • 并发:指系统能够同时处理多个任务的能力。在单核 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 仍需要填写。

#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);
}

int main(int argc, const char *argv[]) {
    pthread_t tid = -1; // 用于存储线程号的变量
    int num = 520;

    if (pthread_create(&tid, NULL, task, &num) != 0) {
        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) {
        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());
    sleep(3);
    pthread_exit(NULL);
}

int main() {
    pthread_t tid = -1;

    // 创建一个线程
    if (pthread_create(&tid, NULL, task, NULL) != 0) {
        printf("pthread_create error\n");
        return -1;
    }

    printf("主线程,tid = %#x\n", tid);

    // 方法 1:使用 pthread_join(tid, NULL); // 前 3 秒,主线程处于休眠等待分支线程的结束

    // 方法 2:将线程设置成分离态 (非阻塞)
    pthread_detach(tid);

    sleep(5);
    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 <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 += 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. Linux/C++多线程编程入门
  2. 前言:为什么需要多线程?
  3. 多线程基础概念
  4. 一、进程与线程的区别
  5. 二、进程与线程的关系
  6. 三、多线程的优缺点
  7. 优点
  8. 缺点
  9. 多线程编程
  10. 一、创建线程:pthread_create
  11. 向线程体中传递单个数据
  12. 向线程体中传入多个数据
  13. 二、线程号的获取:pthread_self
  14. 三、线程的退出函数:pthread_exit
  15. 四、线程的资源回收:pthread_join
  16. 五、线程分离态:pthread_detach
  17. 小问题
  18. 方法 1:使用 pthread_exit()
  19. 方法 2:让主线程等待(但不阻塞)
  20. 六、多线程编程的小练习
  21. 结语
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 大模型 AI 产品经理全学习路线:从基础到实战
  • BK7258 与 LiveKit WebRTC 端侧适配实战
  • Spring Boot 消息队列与异步通信
  • 前端调用 AI 接口全流程及具体案例
  • VSCode Copilot 登录异常排查与修复指南
  • JDK 安装和环境配置教程
  • 机器人架构搭建核心准则:先论文论证,后工程落地
  • Ling Studio 一站式 AI 生产力平台功能与实战指南
  • 吴恩达机器学习:逻辑回归算法详解
  • FastGPT 结合 MCP 协议构建工具增强型智能体实践
  • 选择排序算法详解:原理、代码与优化
  • Docker 存储卷核心概念、类型与操作指南
  • Python 学习路线图:从入门到项目实战
  • Flutter inappwebview_cookie_manager 鸿蒙适配:Web 容器 Cookie 隔离方案
  • 大模型技术学习路线:理论、实践与应用指南
  • 基于 Java Swing 的个人所得税计算模拟器实战
  • C++ STL 容器:基于红黑树模拟实现 map 和 set
  • C++ STL string 类从零实现详解
  • Android 复刻 Apple AppStore 卡片转场动画实现详解
  • 二分查找算法详解与实战模板

相关免费在线工具

  • 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