Linux线程控制——从线程创建到线程分离(第一部分:线程创建)
在前序文章中,我们已经讲解了线程的基础概念,以及线程与进程的核心区别和关联。
从本文开始,我们正式聚焦线程生命周期控制的核心知识点。
所谓线程控制,本质是管控线程从创建、运行、终止、等待回收到资源释放的完整生命周期全过程。
线程完整生命周期流程如下:
pthread_create()
│
▼
创建线程
│
▼
开始执行任务
│
▼
return / pthread_exit()
│
▼
pthread_join() 或 pthread_detach()
│
▼
释放资源
作为线程生命周期的第一步,本文将详细讲解线程创建的原理、函数用法、代码实践以及开发中的高频踩坑点。
一、pthread_create——线程创建核心函数
Linux 系统中,线程创建依赖 POSIX 标准线程库(pthread)提供的 pthread_create() 函数,这是创建用户态线程的核心接口。
函数标准原型如下:
参数详细说明
(1)thread:线程ID输出参数
pthread_t *thread
该参数为输出型参数,用于存储新创建线程的唯一线程ID。
线程创建成功后,我们可通过该线程ID,对目标线程执行等待回收、线程分离、线程控制等后续操作。
基础使用示例:
pthread_t tid;
// 创建线程,tid 用于接收新线程ID
pthread_create(&tid, nullptr, ThreadRun, nullptr);
(2)attr:线程属性参数
const pthread_attr_t *attr
该参数用于配置线程的自定义属性,包括线程栈大小、调度策略、分离状态、优先级等高级特性。
日常基础开发中,直接传入 nullptr(C++)或 NULL(C语言)即可,表示使用系统默认线程属性。
基础使用示例:
pthread_create(&tid, nullptr, ThreadRun, nullptr);
后续讲解线程高级特性时,我们再详细介绍自定义线程属性的配置方法,现阶段统一使用默认属性即可。
(3)start_routine:线程入口函数
void *(*start_routine)(void *)
这是线程的核心任务入口函数,也是线程真正执行业务逻辑的载体。
需要重点注意:新线程创建成功后,不会从 main() 函数开始执行,而是直接从该入口函数启动运行。
自定义线程函数规范示例:
// 标准线程入口函数格式
void* ThreadRun(void* args)
{
// 线程业务逻辑
return nullptr;
}
创建线程时绑定入口函数:
pthread_create(&tid, nullptr, ThreadRun, nullptr);
上述代码执行后,新线程会自动调用ThreadRun(nullptr) 执行任务。
线程执行逻辑流程图解:
主线程
│
├─────────────── 继续向下执行 main() 逻辑
│
└──────────────► 新线程创建完成
│
▼
执行 ThreadRun() 任务函数
(4)arg:线程入口函数参数
void *arg
该参数是传递给线程入口函数的自定义参数,最终会完整传入 start_routine 的形参中。
简单传参示例:
// 定义主线程局部变量
int number = 100;
// 将变量地址作为参数传递给新线程
pthread_create(&tid, nullptr, ThreadRun, &number);
线程内部接收并解析参数:
void* ThreadRun(void* args)
{
// 强制类型转换,还原参数类型
int* p = (int*)args;
cout << *p << endl;
return nullptr;
}
程序最终输出结果:
![]()
返回值说明

pthread_create() 的返回值规则简洁清晰:
-
创建成功:返回
0 -
创建失败:返回非0错误码
这里有一个极易踩坑的知识点:Linux 普通系统调用失败通常返回 -1,但 pthread 系列函数不遵循该规则。
pthread 函数失败时会直接返回具体的错误码,因此不推荐使用 perror() 打印错误,更推荐标准写法:
不推荐写法(适配性差):
if(pthread_create(...) != 0)
{
perror("pthread_create");
return 1;
}
推荐标准写法:
int ret = pthread_create(&tid, nullptr, ThreadRun, nullptr);
if(ret != 0)
{
// 根据错误码解析异常信息
cout << "线程创建失败:" << strerror(ret) << endl;
return 1;
}
二、首个完整线程程序
我们通过一段极简代码,直观感受主线程与子线程的并发执行特性:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 子线程执行函数
void* ThreadRun(void* args)
{
while(true)
{
cout << "I am thread." << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
// 创建子线程
pthread_create(&tid, nullptr, ThreadRun, nullptr);
// 主线程循环执行
while(true)
{
cout << "I am main thread." << endl;
sleep(1);
}
return 0;
}
程序运行说明:
线程调度由操作系统内核全权决定,调度顺序具有随机性,因此每次运行的打印顺序都不固定。
正常运行结果示例1:
正常运行结果示例2(完全合规):
两种输出均为正常现象,是操作系统 CPU 调度随机性的正常表现。
三、线程的执行启动原理
很多初学者都会疑惑:调用 pthread_create() 后,我们没有手动调用 ThreadRun(),为什么线程函数会自动执行?
核心原理:pthread_create() 调用后,操作系统会完成一系列线程初始化操作,全程无需开发者干预:
-
为新线程创建独立的线程控制块(TCB),记录线程状态、上下文、优先级等信息;
-
将自定义的
ThreadRun函数地址,设置为新线程的执行入口地址; -
将新线程加入系统调度队列,等待 CPU 调度执行。
当操作系统调度器选中该线程时,CPU 会直接跳转到入口函数地址,自动执行线程业务逻辑。
完整流程梳理:
调用 pthread_create()
│
▼
创建线程控制块TCB
│
▼
记录线程入口函数地址
│
▼
线程进入调度等待状态
│
▼
CPU调度命中线程
│
▼
自动执行 ThreadRun() 函数
简单总结:线程函数是由操作系统调度自动触发执行,而非开发者手动调用。
四、线程参数传递的全面用法
初学者普遍存在一个误区:认为 void* arg 只能传递单个基础变量。
实际上,void* 是无类型通用指针,可以指向任意内存地址,支持传递几乎所有数据类型,包括:普通变量、数组、结构体、类对象、STL容器、自定义对象等。
1、传递普通基础变量
适用于简单场景,仅需传递单个数值的情况:
// 主线程定义变量
int number = 100;
// 传递变量地址
pthread_create(&tid, nullptr, ThreadRun, &number);
线程内解析参数:
void* ThreadRun(void* args)
{
int* p = (int*)args;
cout << *p << endl;
return nullptr;
}
2、传递结构体
实际开发中,若需要传递多个参数,最规范的方式是将所有参数封装为结构体,一次性传递,代码可读性和可维护性更强。
// 自定义任务参数结构体
struct Task
{
int id; // 任务ID
string filename;// 文件名称
int timeout; // 超时时间
};
int main()
{
Task task;
// 初始化参数
task.id = 1;
task.filename = "video.mp4";
task.timeout = 30;
// 传递结构体地址
pthread_create(&tid, nullptr, ThreadRun, &task);
return 0;
}
线程内解析结构体参数:
void* ThreadRun(void* args)
{
// 强制转换为结构体指针
Task* task = (Task*)args;
cout << "任务ID:" << task->id << endl;
cout << "文件名称:" << task->filename << endl;
return nullptr;
}
3、传递类对象
面向对象开发中,可直接传递自定义类对象,适配业务化场景:
// 自定义下载任务类
class DownloadTask
{
public:
int id;
string url; // 下载链接
string savePath; // 保存路径
};
int main()
{
DownloadTask task;
// 初始化对象参数
task.url = "https://xxx.mp4";
task.savePath = "./download/";
// 传递类对象地址
pthread_create(&tid, nullptr, ThreadRun, &task);
return 0;
}
线程内解析类对象参数:
void* ThreadRun(void* args)
{
DownloadTask* task = (DownloadTask*)args;
cout << "下载地址:" << task->url << endl;
return nullptr;
}
核心总结:void* 的本质是接收任意数据类型的内存地址,支持批量参数传递。项目开发中,优先使用结构体/类对象封装多参数,是最标准的开发范式。
五、线程参数传递的经典致命陷阱
参数传递中有一个高频必踩坑:多线程共享同一块临时内存,导致数据错乱、参数覆盖。
错误示例代码:
char name[64];
pthread_t tid[5];
for(int i = 0; i < 5; i++)
{
// 循环覆盖同一内存的数据
sprintf(name, "Thread-%d", i);
// 所有线程传递同一个内存地址
pthread_create(&tid[i], nullptr, ThreadRun, name);
}
错误原因分析
代码看似逻辑无误,但存在严重的数据竞争问题:
全局/局部数组 name 仅有一块固定内存,所有创建的子线程,接收的都是同一个内存地址,而非独立数据。
内存逻辑图解:
name 共享内存
┌────────────────┐
│ 数据实时被覆盖 │
└────────────────┘
▲
┌────────┼─────────┐
│ │ │
线程1 线程2 线程3
由于线程调度存在延迟,循环创建线程的速度远快于线程执行速度。当子线程开始执行任务、读取内存数据时,主线程的循环早已完成,name 内存中的数据已经被反复覆盖。
最终结果:所有线程大概率全部输出最后一次循环的结果:
问题根源:多线程共享可修改的临时内存,导致数据被覆盖,并非线程本身故障。
六、标准解决方案:为线程分配独立内存数据
解决核心思路:杜绝多线程共享同一块可修改内存,为每个线程单独分配堆内存,保证数据独立互不干扰。
方案1:动态分配字符数组(简单场景)
pthread_t tid[5];
for(int i = 0; i < 5; i++)
{
// 每次循环分配独立堆内存
char* name = new char[64];
sprintf(name, "Thread-%d", i);
// 传递独立内存地址
pthread_create(&tid[i], nullptr, ThreadRun, name);
}
线程内使用完毕后,手动释放堆内存,避免内存泄漏:
void* ThreadRun(void* args)
{
char* name = (char*)args;
cout << name << endl;
// 释放当前线程的独立内存
delete[] name;
return nullptr;
}
方案2:封装结构体/类对象(项目推荐)
复杂业务场景下,推荐使用结构体封装参数,扩展性更强,也是企业级开发的标准写法:
struct Task
{
int id;
string filename;
};
pthread_t tid[5];
for(int i = 0; i < 5; i++)
{
// 为每个线程单独创建结构体对象
Task* task = new Task;
task->id = i;
task->filename = "file_" + to_string(i);
// 传递独立对象地址
pthread_create(&tid[i], nullptr, ThreadRun, task);
}
该方案彻底解决了数据覆盖问题,同时支持多参数传递,代码可维护性、可扩展性远优于基础传参方式。
本章小结
本文作为线程控制系列第一篇,重点讲解了线程创建的完整知识体系,核心要点总结如下:
-
pthread_create()是 Linux 创建用户线程的核心函数,负责初始化线程、绑定入口函数、加入系统调度队列; -
子线程从自定义入口函数开始执行,与主线程并发运行,执行时机由操作系统调度决定;
-
四大参数各司其职:线程ID存储、线程属性配置、任务入口绑定、自定义参数传递;
-
void*是通用指针,支持传递任意类型数据,可通过结构体/类对象实现多参数批量传递; -
多线程传参核心禁忌:禁止共享可修改的临时内存,极易引发数据覆盖、数据竞争问题;
-
标准开发规范:通过堆内存为每个线程分配独立数据,用完手动释放,兼顾安全性和扩展性。
下一章预告:我们将详解线程终止的四种方式(return、pthread_exit()、exit()、pthread_cancel()),深度对比各类终止方式的区别、适用场景和底层差异。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)