在前序文章中,我们已经讲解了线程的基础概念,以及线程与进程的核心区别和关联。

从本文开始,我们正式聚焦线程生命周期控制的核心知识点。

所谓线程控制,本质是管控线程从创建、运行、终止、等待回收到资源释放的完整生命周期全过程。

线程完整生命周期流程如下:

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() 调用后,操作系统会完成一系列线程初始化操作,全程无需开发者干预:

  1. 为新线程创建独立的线程控制块(TCB),记录线程状态、上下文、优先级等信息;

  2. 将自定义的 ThreadRun 函数地址,设置为新线程的执行入口地址;

  3. 将新线程加入系统调度队列,等待 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()),深度对比各类终止方式的区别、适用场景和底层差异。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐