
⭐️在这个怀疑的年代,我们依然需要信仰。
个人主页:YYYing.
⭐️Linux/C++进阶 系列专栏:【从零开始的linux/c++进阶编程】
系列上期内容:【Linux/C++多进程篇(二) 】linux系统编程之进程间通信 (IPC)
系列下期内容:暂无
目录
[📖 优点](#📖 优点)
[📖 缺点](#📖 缺点)
[📖 向线程体中传递单个数据](#📖 向线程体中传递单个数据)
[📖 向线程体中传入多个数据](#📖 向线程体中传入多个数据)
[📖 小问题](#📖 小问题)
[方法1:使用 pthread_exit()](#方法1:使用 pthread_exit())

前言:为什么需要多线程?
想象一下,你现在是一个厨师,需要同时做三件事:切菜、煮汤、接电话。如果你只能一件一件地做,顾客会等得不耐烦,汤可能煮干,电话也可能被挂断。在计算机世界里,程序也常常需要同时处理多个任务------比如一边响应用户操作,一边下载文件,一边更新界面。其也是为了实现多任务并发执行的问题的,能够实现多个阻塞任务同时执行
多线程就是让一个程序拥有多个执行流,可以"同时"做多件事。这里的"同时"有两种含义:
-
并发:指系统能够同时处理多个任务的能力。在单核 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, 后面还有更精彩的内容,希望各位能多多关注支持一下主包。
无限进步,我们下次再见!
---⭐️ 封面自取 ⭐️---
