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

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

想象一下,你现在是一个厨师,需要同时做三件事:切菜、煮汤、接电话。如果你只能一件一件地做,顾客会等得不耐烦,汤可能煮干,电话也可能被挂断。在计算机世界里,程序也常常需要同时处理多个任务——比如一边响应用户操作,一边下载文件,一边更新界面。其也是为了实现多任务并发执行的问题的,能够实现多个阻塞任务同时执行。
多线程就是让一个程序拥有多个执行流,可以'同时'做多件事。这里的'同时'有两种含义:
多线程的核心价值在于提高程序的响应性、资源利用率和吞吐量。
多线程(LWP 轻量版进程):线程是粒度更小的任务执行单元。
进程是资源分配的基本单位,而线程是任务进行任务调度的最小单位。
一个进程可以拥有多个线程,同一个进程中的多个线程共享进程的资源。由于线程是共用进程的资源,所以对于线程的切换而言,开销较小,但多个线程使用的是同一个进程的资源,那么就会导致每个进程使用资源时,产生资源抢占问题,没有多进程安全。
每个进程至少有一个线程:主线程。
只要有一个线程中退出了进程,那么所有的线程也就结束了,主线程结束后,整个进程也就结束了。
多个线程执行顺序:没有先后顺序,按时间片轮询,上下文切换,抢占 CPU 的方式进行。
我们先讲 C/C++ 的多线程编程,至于 C++ 的线程支持库我们后面也会讲。
再讲之前多提一嘴,由于 C 库没有提供有关多线程的相关操作,对于多线程编程要依赖于第三方库——头文件:#include<pthread.h>,且编译时:需要加上 -lpthread 选项,链接上对于的线程支持库。
| 函数原型 | 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_t pthread_self(void); |
|---|---|
| 头文件 | pthread.h |
| 功能 | 获取当前线程的线程号 |
| 参数说明 | 无 |
| 返回值 | 返回调用线程的 id 号,不会失败 |
| 函数原型 | void pthread_exit(void *retval); |
|---|---|
| 头文件 | pthread.h |
| 功能 | 退出当前线程 |
| 参数说明 | 表示退出时的状态,一般填 NULL |
| 返回值 | 无 |
| 函数原型 | int pthread_join(pthread_t thread, void **retval); |
|---|---|
| 头文件 | pthread.h |
| 功能 | 阻塞回收指定线程的资源 |
| 参数说明 | 参数 1:要回收的线程线程号 参数 2:线程退出时的状态,一般填 NULL |
| 返回值 | 成功返回 0,失败返回一个错误码 |
| 函数原型 | 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;
}
那我相信肯定也会有人发现,那分离进程如果没运行完,主程序退出后,那么此时分离进程会被回收吗?
答案是:会的,但是线程不会有机会执行自己的清理代码。也就是不会自动'回收分离进程',而是整个进程会被强制终止,所有线程(包括分离线程)都会被立即杀死。
所以面对这种情况,我们有两种解决方式:
int main() {
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
pthread_detach(tid); // 只退出主线程,不终止进程
pthread_exit(NULL); // 进程继续,分离线程可以运行完成
// 这里不会执行
return 0;
}
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)以及无锁编程。
在下一篇博客中,我们将深入探讨如何解决竞态条件,揭开互斥锁与同步机制的神秘面纱。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online