C高级:关于多线程编程的学习记录

多线程是 Linux 系统编程中实现并发执行的核心手段。相比多进程方案,线程更轻量、切换更快、共享数据更方便。本文从线程的基本概念讲起,对比线程与进程的区别,然后深入讲解 POSIX 线程库(pthreads)中最常用的三个函数:pthread_create()pthread_join()pthread_detach(),每个参数逐一拆解,配合完整可编译的示例代码,以下是我在职坐标的学习记录:

一、线程是什么

1.1 基本概念

线程(Thread)是进程中的一个执行单元,是 CPU 调度和分派的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源(地址空间、文件描述符、全局变量等),但每个线程拥有自己独立的栈空间寄存器状态程序计数器(PC)

可以这样理解:进程就像一家公司,拥有办公场地、设备、资金等资源;而线程就是公司里的员工,每个人有自己的工位(栈)和手头的工作(寄存器),但共用公司的会议室、打印机等公共设施(堆、全局变量、文件描述符)。

1.2 线程的组成

每个线程都拥有以下独立的资源:

  • 线程 ID(TID):在进程范围内唯一标识该线程。
  • 程序计数器(PC):记录当前执行到指令的哪一条。
  • 寄存器集合:保存线程运行时的中间状态。
  • 栈(Stack):用于存放局部变量、函数调用帧、返回地址等。每个线程有独立的栈,默认大小通常为 8MB(可通过线程属性修改)。

而以下资源是同一进程内所有线程共享的:

  • 代码段(Text Segment)
  • 数据段(Data Segment)和 BSS 段
  • 堆(Heap)
  • 文件描述符表
  • 信号处理函数
  • 当前工作目录
  • 用户 ID 和组 ID

1.3 为什么需要线程

使用多线程的主要动机包括:

  • 提高并发性:多个线程可以同时执行不同的任务(在多核 CPU 上真正并行,在单核 CPU 上通过时间片轮转实现并发)。
  • 降低开销:创建线程的开销远小于创建进程(不需要复制地址空间和文件描述符表)。
  • 共享数据方便:同一进程内的线程可以直接通过全局变量或堆内存交换数据,不需要管道、共享内存等 IPC 机制。
  • 响应性:在 GUI 程序中,可以把耗时操作放到后台线程,保持主线程(UI 线程)的响应。

二、线程与进程的区别

2.1 核心对比

对比维度 进程(Process) 线程(Thread)
定义 操作系统资源分配的基本单位 CPU 调度和执行的基本单位
地址空间 每个进程拥有独立的虚拟地址空间 同一进程内的线程共享地址空间
资源开销 创建和销毁开销大(需要复制页表、文件描述符表等) 创建和销毁开销小
切换开销 上下文切换开销大(需要切换页表、刷新 TLB) 上下文切换开销小(同一地址空间内切换)
数据共享 进程间数据隔离,需要 IPC 机制(管道、共享内存等) 线程间直接共享全局变量和堆内存
同步问题 不存在数据竞争(天然隔离) 存在数据竞争,需要互斥锁、条件变量等同步机制
健壮性 一个进程崩溃不影响其他进程 一个线程崩溃可能导致整个进程挂掉
创建方式 fork() / vfork() pthread_create()
通信方式 管道、消息队列、共享内存、Socket、信号 直接读写共享变量(需加锁)、条件变量

2.2 什么时候用线程,什么时候用进程

  • 用线程:多个任务需要频繁共享数据、对性能要求高、任务之间关联紧密(如 Web 服务器为每个客户端连接创建一个工作线程)。
  • 用进程:任务之间相互独立、需要更好的隔离性和健壮性、不需要频繁共享数据(如 Shell 管道中的多个命令)。

三、POSIX 线程库(pthreads)

Linux 下的标准线程库是 pthreads(POSIX threads),它是一组遵循 POSIX 标准的 C 语言 API。使用时需要包含头文件 <pthread.h>,并在编译时链接线程库 -lpthread

#include <pthread.h>
// 编译时加 -lpthread
// gcc program.c -o program -lpthread

线程的核心类型是 pthread_t,它是一个不透明的类型(在某些系统上是 unsigned long),用于标识一个线程。

四、pthread_create() —— 创建线程

4.1 函数原型

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);

4.2 函数作用

pthread_create() 创建一个新的线程,新线程从 start_routine 函数开始执行。调用成功后,新线程与调用线程(父线程)并发运行。

4.3 参数详解

参数 类型 方向 说明
thread pthread_t * 输出 指向 pthread_t 类型变量的指针。函数成功返回后,这个变量会被填充为新线程的 线程 ID(TID),后续通过它来引用这个线程(如 pthread_join()pthread_detach())。
attr const pthread_attr_t * 输入 指向线程属性对象的指针,用于设置线程的各种属性(栈大小、调度策略、分离状态等)。通常传 NULL,表示使用默认属性。如果需要自定义,先用 pthread_attr_init() 初始化属性对象。
start_routine void *(*)(void *) 输入 新线程的入口函数(线程开始执行的函数)。函数签名必须是 void *func(void *arg):接受一个 void * 参数,返回一个 void * 指针。返回值可以通过 pthread_join() 被其他线程获取。
arg void * 输入 传递给 start_routine 的参数。可以是任意类型的数据,通过强制类型转换传入。如果不需要传参,可以传 NULL。如果需要传递多个参数,通常的做法是将参数封装成一个结构体,然后传递结构体的指针。

关于 start_routine 的参数和返回值:

// 线程入口函数的标准形式
void *thread_func(void *arg) {
    // arg 就是 pthread_create 的第四个参数
    int *num = (int *)arg;  // 假设传入的是 int 指针
    printf("收到参数: %d\n", *num);
    
    // 返回值可以通过 pthread_join 获取
    return NULL;  // 或者返回一个指针
}

关于 arg 的常见陷阱:

// ❌ 错误:传递局部变量地址(线程执行时局部变量可能已失效)
void create_thread_bad() {
    int value = 42;
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, &value);
    // 函数返回后,value 的栈空间被回收,线程可能读到垃圾值
}

// ✅ 正确:使用堆分配或静态/全局变量
void create_thread_good() {
    int *value = malloc(sizeof(int));
    *value = 42;
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, value);
}

4.4 返回值

  • 成功:返回 0
  • 失败:返回一个正的错误码(不是 -1,这一点和大多数系统调用不同)。常见的错误码包括:
    • EAGAIN:系统资源不足(如线程数达到上限)。
    • EINVALattr 参数无效。
    • EPERM:没有权限设置指定的调度策略。

注意pthread_create() 失败时不会设置 errno,而是直接返回错误码。因此不能用 perror(),而应该用 strerror() 来打印错误信息:

int ret = pthread_create(&tid, NULL, func, NULL);
if (ret != 0) {
    fprintf(stderr, "pthread_create failed: %s\n", strerror(ret));
}

4.5 示例代码

基本用法:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 线程入口函数
void *print_hello(void *arg) {
    char *name = (char *)arg;
    printf("Hello from thread, name = %s\n", name);
    printf("线程 ID: %lu\n", (unsigned long)pthread_self());
    return NULL;
}

int main() {
    pthread_t tid;

    // 创建线程,使用默认属性,传入字符串参数
    int ret = pthread_create(&tid, NULL, print_hello, "Worker");
    if (ret != 0) {
        printf("创建线程失败: %s\n", strerror(ret));
        return 1;
    }

    printf("主线程: 已创建子线程,TID = %lu\n", (unsigned long)tid);

    // 等待子线程结束(下一节详细讲解)
    pthread_join(tid, NULL);

    printf("主线程: 子线程已结束\n");
    return 0;
}

传递多个参数(结构体方式):

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

// 定义参数结构体
struct thread_args {
    int id;
    char *name;
    double score;
};

void *worker(void *arg) {
    struct thread_args *args = (struct thread_args *)arg;
    printf("ID: %d, Name: %s, Score: %.2f\n",
           args->id, args->name, args->score);
    
    // 释放参数内存
    free(args);
    return NULL;
}

int main() {
    pthread_t tid;
    
    // 动态分配参数结构体
    struct thread_args *args = malloc(sizeof(struct thread_args));
    args->id = 1;
    args->name = "Alice";
    args->score = 95.5;

    pthread_create(&tid, NULL, worker, args);
    pthread_join(tid, NULL);

    return 0;
}

五、pthread_join() —— 等待线程结束并回收资源

5.1 函数原型

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

5.2 函数作用

pthread_join() 使调用线程阻塞等待指定的线程结束。当目标线程终止后,pthread_join() 会回收该线程的资源(栈、线程描述符等),并可选地获取目标线程的返回值。

这个函数的角色类似于进程编程中的 waitpid()——如果一个线程结束后没有被 join 也没有被 detach,它就会变成一个"僵尸线程",持续占用系统资源。因此,每个默认创建的线程都必须通过 pthread_join()pthread_detach() 来回收。

5.3 参数详解

参数 类型 方向 说明
thread pthread_t 输入 要等待的目标线程的 TID,即 pthread_create() 填充的那个值。注意这里传的是值,不是指针。
retval void ** 输出 一个指向指针的指针,用于接收目标线程的返回值。目标线程通过 return 语句或 pthread_exit() 返回的值,会存储到 *retval 中。如果不需要获取返回值,可以传 NULL

关于 retval 的使用:

void *retval;
pthread_join(tid, &retval);
// 如果子线程 return (void*)42;  则 retval 的值为 42

// 如果不需要返回值
pthread_join(tid, NULL);

重要注意事项:

  • retval 不能指向目标线程的局部变量(栈上变量),因为线程结束后其栈空间会被释放。返回值应该是堆上的地址、静态变量地址,或者直接将整数值强转为指针。
  • 只能对默认属性(joinable) 创建的线程调用 pthread_join()。如果线程已经被设置为分离状态(detached),调用 pthread_join() 会返回 EINVAL 错误。
  • 同一个线程只能被 join 一次。对已经 join 过的线程再次调用 pthread_join() 会导致未定义行为(通常返回 ESRCH 或崩溃)。
  • pthread_join()阻塞调用——调用线程会一直等待,直到目标线程结束。如果需要非阻塞方式,可以使用 pthread_tryjoin_np()(Linux 特有,非标准)。

5.4 返回值

  • 成功:返回 0retval 被填充为目标线程的返回值。
  • 失败:返回正的错误码。常见错误码:
    • ESRCH:找不到指定 TID 的线程。
    • EINVAL:目标线程是分离状态(detached),或者已经被其他线程 join。
    • EDEADLK:检测到死锁(如线程试图 join 自己)。

5.5 示例代码

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

// 线程函数:计算 1~n 的和,通过返回值传出结果
void *sum_worker(void *arg) {
    int n = *(int *)arg;
    long long sum = 0;
    for (int i = 1; i <= n; i++) {
        sum += i;
    }
    
    // 注意:不能返回局部变量的地址
    // 方案 1:将结果放到堆上
    long long *result = malloc(sizeof(long long));
    *result = sum;
    return result;
}

int main() {
    pthread_t tid;
    int n = 1000000;
    
    pthread_create(&tid, NULL, sum_worker, &n);
    
    void *retval;
    pthread_join(tid, &retval);
    
    long long sum = *(long long *)retval;
    printf("1 ~ %d 的和 = %lld\n", n, sum);
    
    free(retval);  // 释放堆上的返回值
    return 0;
}

多线程式示例(创建多个线程并分别 join):

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

#define NUM_THREADS 4

struct task_arg {
    int thread_id;
    int start;
    int end;
};

void *partial_sum(void *arg) {
    struct task_arg *ta = (struct task_arg *)arg;
    long long sum = 0;
    for (int i = ta->start; i <= ta->end; i++) {
        sum += i;
    }
    printf("线程 %d: 计算 %d ~ %d 的和 = %lld\n",
           ta->thread_id, ta->start, ta->end, sum);
    
    long long *result = malloc(sizeof(long long));
    *result = sum;
    return result;
}

int main() {
    pthread_t threads[NUM_THREADS];
    struct task_arg args[NUM_THREADS];
    int range = 250000;

    // 创建 4 个线程,每个线程计算一段区间的和
    for (int i = 0; i < NUM_THREADS; i++) {
        args[i].thread_id = i;
        args[i].start = i * range + 1;
        args[i].end = (i + 1) * range;
        pthread_create(&threads[i], NULL, partial_sum, &args[i]);
    }

    // 依次 join 所有线程,收集结果
    long long total = 0;
    for (int i = 0; i < NUM_THREADS; i++) {
        void *retval;
        pthread_join(threads[i], &retval);
        total += *(long long *)retval;
        free(retval);
    }

    printf("总和 = %lld\n", total);
    return 0;
}

六、pthread_detach() —— 分离线程(自动回收资源)

6.1 函数原型

#include <pthread.h>

int pthread_detach(pthread_t thread);

6.2 函数作用

pthread_detach() 将指定的线程设置为分离状态(detached)。处于分离状态的线程在终止时会自动释放自己的所有资源(栈、线程描述符等),不需要其他线程来 join 它。

这个函数的使用场景是:当你创建了一个"后台任务"线程,只关心它执行任务,不关心它的返回值,也不想在它结束后手动 join 时,就可以将它分离。

6.3 参数详解

参数 类型 方向 说明
thread pthread_t 输入 要设置为分离状态的目标线程的 TID。可以是其他线程的 TID,也可以在线程内部调用 pthread_detach(pthread_self()) 将自己分离。

6.4 分离线程 vs 可连接线程

对比维度 可连接线程(Joinable,默认) 分离线程(Detached)
资源回收 必须由其他线程调用 pthread_join() 回收 线程终止时自动回收
获取返回值 可以通过 pthread_join() 获取 不能获取返回值
能否 join 可以 不可以(调用 pthread_join() 会返回 EINVAL
适用场景 需要等待线程结果、需要获取返回值 后台任务、不关心结果、不希望管理生命周期

两种设置分离状态的方式:

// 方式 1:创建后调用 pthread_detach()
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
pthread_detach(tid);

// 方式 2:创建时通过线程属性设置
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

pthread_t tid;
pthread_create(&tid, &attr, worker, NULL);
pthread_attr_destroy(&attr);  // 用完销毁属性对象

// 方式 3:在线程内部将自己分离
void *worker(void *arg) {
    pthread_detach(pthread_self());
    // 执行任务...
    return NULL;  // 线程结束时自动回收
}

6.5 返回值

  • 成功:返回 0
  • 失败:返回正的错误码。常见错误码:
    • ESRCH:找不到指定 TID 的线程。
    • EINVAL:目标线程已经是分离状态,或者已经被 join。

6.6 注意事项

  • 一个线程只能被 detach 一次。对已经处于分离状态的线程再次调用 pthread_detach() 会返回 EINVAL
  • 分离状态的线程不能被 join。如果需要对一个已经 detach 的线程调用 pthread_join(),会失败。
  • 一旦线程被分离,就没有办法将它恢复为可连接状态。

6.7 示例代码

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 后台日志线程
void *logger(void *arg) {
    // 将自己设置为分离状态
    pthread_detach(pthread_self());

    for (int i = 0; i < 5; i++) {
        printf("[Logger] 正在记录日志... (%d/5)\n", i + 1);
        sleep(1);
    }
    printf("[Logger] 日志记录完成,线程将自动回收资源\n");
    return NULL;
}

int main() {
    pthread_t tid;
    
    pthread_create(&tid, NULL, logger, NULL);
    
    // 主线程继续做其他事情,不需要 join logger
    printf("[Main] 后台日志线程已启动,主线程继续执行...\n");
    
    // 主线程做一些工作
    sleep(7);
    
    printf("[Main] 主线程结束\n");
    // logger 线程会在结束时自动回收资源
    return 0;
}

七、pthread_exit() —— 线程退出

虽然不在题目要求内,但作为线程编程的基础知识,这里简要提一下。

#include <pthread.h>

void pthread_exit(void *retval);

线程可以通过三种方式终止:

  1. 从入口函数 return:返回一个 void * 值作为线程的退出状态。
  2. 被其他线程 pthread_cancel():强制终止目标线程。
  3. 调用 pthread_exit():主动终止自己,并设置返回值。
void *worker(void *arg) {
    // 做一些工作...
    
    // 方式 1:直接 return
    // return NULL;
    
    // 方式 2:调用 pthread_exit
    pthread_exit(NULL);  // 等价于 return NULL
}

注意pthread_exit() 不会关闭进程打开的文件描述符,也不会释放 malloc 分配的内存(除非在线程内部手动释放)。它只是终止当前线程的执行。如果在 main() 中调用 pthread_exit(),主线程会终止,但其他线程仍然可以继续运行,直到所有线程都退出后进程才会真正结束。

八、三个函数的对比总结

函数 作用 一句话记忆
pthread_create() 创建一个新线程并开始执行 create a new thread
pthread_join() 阻塞等待线程结束,回收资源并获取返回值 join(汇合)等待线程完成
pthread_detach() 设置线程为分离状态,结束后自动回收资源 detach(分离)让线程自己负责回收

线程生命周期总结:

                    ┌─── pthread_join() ──→ 回收资源,获取返回值
                    │
pthread_create() ──→ 线程运行中 ──→ 线程结束
                    │
                    └─── pthread_detach() ──→ 自动回收资源(无法获取返回值)

九、常见问题 FAQ

Q1:pthread_join()pthread_detach() 可以同时使用吗?
不可以。一个线程要么被 join,要么被 detach,二选一。对同一个线程同时做两件事会导致错误。

Q2:不 join 也不 detach 会怎样?
线程终止后会变成"僵尸线程"(类似僵尸进程),持续占用系统资源(线程描述符、栈空间等)。如果反复创建线程而不回收,最终会导致资源耗尽。

Q3:主线程退出,子线程会怎样?
如果主线程调用 returnexit(),整个进程会终止,所有线程都会被强制结束。如果主线程调用 pthread_exit(),主线程自己退出,但子线程可以继续运行,直到所有线程都退出后进程才终止。

Q4:线程函数中可以调用 exit() 吗?
不建议。exit() 会终止整个进程(包括所有线程),而不仅仅是当前线程。应该使用 pthread_exit()return 来终止单个线程。

十、编译说明

所有使用 pthreads 的程序在编译时都需要链接 -lpthread(或 -pthread):

# 方式 1
gcc program.c -o program -lpthread

# 方式 2(推荐,自动处理链接和宏定义)
gcc program.c -o program -pthread
posted @ 2026-07-26 15:16  Solitude0420  阅读(31)  评论(4)    收藏  举报