跳到主要内容Linux/C++多线程编程入门:核心概念与常用函数详解 | 极客日志C++
Linux/C++多线程编程入门:核心概念与常用函数详解
Linux 环境下 C++ 多线程编程基础教程。涵盖进程与线程区别、并发与并行概念、pthread 库核心函数如 create、join、detach 的使用及参数传递方式。通过文件拷贝实战演示多线程资源管理与同步机制,解析线程生命周期与资源回收策略,为后续互斥锁与同步机制学习奠定基础。
KernelLab1 浏览 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) {
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);
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(10);
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)以及无锁编程。
在下一篇博客中,我们将深入探讨如何解决竞态条件,揭开互斥锁与同步机制的神秘面纱。
相关免费在线工具
- 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