【Linux开发】01多线程编程:线程的创建与运行

一、为什么需要线程?

1.1 回顾多进程的缺点

我们之前学习了多进程服务器:父进程 fork 出子进程来处理客户端请求。这种方式虽然能实现并发,但存在一些问题:

  • 资源开销大:每个进程都有独立的地址空间,创建和切换进程的成本较高。
  • 通信麻烦:进程间通信(IPC)需要管道、共享内存等机制,比较繁琐。
  • 数据共享困难:父子进程虽然 fork 时复制了数据,但之后各自独立,要共享数据必须用 IPC。

1.2 线程的优势

线程 (Thread)是进程内的一个执行流,它共享进程的地址空间(代码段、数据段、堆等),但拥有独立的栈和寄存器上下文。

对比项 进程 线程
地址空间 独立 共享(除了栈)
创建开销
切换开销
数据共享 需要 IPC 直接访问全局变量
并发性 多进程可多核 多线程也可多核
稳定性 一个进程崩溃不影响其他 一个线程崩溃可能导致整个进程崩溃

因此,在需要大量并发且频繁共享数据的场景下(如 Web 服务器、GUI 程序),线程是更合适的选择。


二、线程的基本概念

  • 线程 ID :每个线程有一个唯一的标识符(pthread_t 类型),类似于进程的 PID。
  • 线程主函数 :线程开始执行的地方,函数原型为 void* thread_func(void* arg)
  • 线程的终止 :线程函数返回、调用 pthread_exit() 或被取消。
  • 线程的回收 :类似进程的 wait,需要调用 pthread_join 等待线程结束并回收资源,否则可能造成僵尸线程(类似僵尸进程)。

三、创建线程:pthread_create

3.1 函数原型

c 复制代码
#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);
  • thread:输出参数,成功时返回新线程的 ID。
  • attr :线程属性(如栈大小、调度策略),通常传 NULL 表示使用默认属性。
  • start_routine :线程主函数指针,格式必须为 void* func(void*)
  • arg:传递给线程主函数的参数(可以是任意指针)。
  • 返回值:成功返回 0,失败返回非零错误码(不是 -1)。

3.2 线程主函数格式

c 复制代码
void* my_thread_func(void *arg) {
    // 线程执行的代码
    return (void*)some_ptr;   // 返回值可以通过 pthread_join 获得
}

3.3 参数传递

由于 argvoid*,可以传递任意类型:

  • 传递基本类型:将值强转为 void*(注意数据长度)。
  • 传递结构体:传结构体指针。
  • 传递多个参数:封装成结构体。

四、等待线程结束:pthread_join

4.1 为什么需要 join?

  • 主线程结束时会终止整个进程,所有子线程也会被强制结束。
  • 为了避免主线程过早退出,需要等待子线程完成。
  • 同时可以获取子线程的返回值,并回收线程资源(防止资源泄漏)。

4.2 函数原型

c 复制代码
int pthread_join(pthread_t thread, void **retval);
  • thread:要等待的线程 ID。
  • retval :二级指针,用于接收线程主函数的返回值(如果不需要可传 NULL)。
  • 返回值:成功返回 0,失败返回错误码。

4.3 注意

  • pthread_join 会阻塞调用线程,直到目标线程终止。
  • 一个线程只能被 join 一次,多次 join 行为未定义。
  • 如果线程已经终止,pthread_join 会立即返回。

五、完整示例:创建线程并等待返回值

下面是一个完整的示例,主线程创建一个子线程,子线程循环打印消息,最后返回一个字符串,主线程打印该字符串并释放内存。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>

// 线程主函数
void* thread_main(void *arg) {
    int i;
    int cnt = *((int*)arg);              // 获取传入的参数(循环次数)
    char *msg = (char*)malloc(sizeof(char) * 50);
    strcpy(msg, "Hello, I'm thread~ \n");

    for (i = 0; i < cnt; i++) {
        sleep(1);                        // 模拟耗时工作
        puts("running thread");
    }

    return (void*)msg;                   // 返回字符串指针
}

int main() {
    pthread_t t_id;
    int thread_param = 5;
    void *thr_ret;

    // 1. 创建线程
    if (pthread_create(&t_id, NULL, thread_main, (void*)&thread_param) != 0) {
        perror("pthread_create");
        return -1;
    }

    // 2. 等待线程结束,并获取返回值
    if (pthread_join(t_id, &thr_ret) != 0) {
        perror("pthread_join");
        return -1;
    }

    // 3. 打印线程返回的消息
    printf("Thread return message: %s\n", (char*)thr_ret);
    free(thr_ret);                       // 释放线程内部分配的内存

    return 0;
}

5.1 编译与运行

由于 pthread 库不是默认链接的,编译时需要加上 -pthread 选项:

bash 复制代码
gcc -pthread thread_example.c -o thread_example
./thread_example

输出

复制代码
running thread
running thread
running thread
running thread
running thread
Thread return message: Hello, I'm thread~ 

5.2 代码详解

行号 解释
pthread_create(&t_id, NULL, thread_main, (void*)&thread_param) 创建线程,参数 thread_param 的地址传给线程函数。
thread_main 中的 int cnt = *((int*)arg) 解引用获取传入的整数值。
malloc 分配字符串 线程内部分配内存,返回给主线程,主线程负责释放。
pthread_join(t_id, &thr_ret) 主线程等待子线程结束,thr_ret 接收返回值。
free(thr_ret) 释放子线程中 malloc 的内存,避免内存泄漏。

六、常见问题与注意事项

6.1 编译时链接错误

错误undefined reference to pthread_create'
原因 :未链接 pthread 库。
解决 :编译时加上 -pthread 选项。

6.2 主线程过早退出

c 复制代码
// 错误示例
int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, func, NULL);
    // 没有 pthread_join,主线程直接退出 → 整个进程结束,子线程被强制终止
}

解决 :调用 pthread_join 等待子线程,或调用 pthread_exit(NULL) 让主线程退出而进程继续(但通常不推荐)。

6.3 传递局部变量作为线程参数

c 复制代码
// 危险示例
void* func(void *arg) {
    int *p = (int*)arg;
    printf("%d\n", *p);   // p 指向的可能是主线程的局部变量,但主线程可能已退出或变量失效
}
int main() {
    int x = 10;
    pthread_create(&tid, NULL, func, &x);
    pthread_join(tid, NULL);   // 这里 x 还在作用域内,安全
    return 0;
}

注意 :如果主线程在子线程使用参数之前就退出了(或局部变量被销毁),则子线程会访问无效内存。因此要么用 pthread_join 等待,要么将参数放在堆上(malloc)。

6.4 返回值内存管理

  • 线程主函数返回的指针通常指向静态数据堆上分配的内存
  • 如果返回局部变量的地址,该内存在线程函数退出后会被释放,造成悬空指针。
  • 最佳实践 :使用 malloc 分配返回值,调用 pthread_join 的线程负责 free

6.5 线程 ID 的类型

pthread_t 可能是一个整数或结构体,不能直接当作整数打印(除非用 %lu 并强制转换,但不可移植)。调试时可使用 pthread_self() 获取当前线程 ID,并打印指针值。


七、总结

7.1 核心函数速查

函数 作用
pthread_create(&tid, NULL, func, arg) 创建新线程
pthread_join(tid, &retval) 等待线程结束并获取返回值
pthread_self() 获取当前线程的 ID
pthread_exit(retval) 主动终止当前线程(类似 return

7.2 线程 vs 进程

项目 进程 线程
数据共享 复杂(需要 IPC) 简单(直接访问全局变量)
创建/切换开销
崩溃影响 其他进程不受影响 整个进程可能崩溃
同步需求 较少(数据独立) 较多(需锁保护共享数据)
相关推荐
jiuri_12152 小时前
OpenHarmony 移植 OpenSSH/sshd
linux·sshd·ohos
慕诗客2 小时前
英伟达Jetson Agx Orin更换开机Logo
linux
蜕变的土豆2 小时前
ABB1200系列机器人配置
运维·服务器·机器人
闻哥2 小时前
Docker Swarm 负载均衡深度解析:VIP vs DNSRR 模式详解
java·运维·jvm·docker·容器·负载均衡
我爱学习好爱好爱2 小时前
Ansible include任务复用 tags ignore_errors
linux·运维·ansible
YMWM_2 小时前
【问题修复】ubuntu24.04打不开windows的D盘
linux
淼淼爱喝水2 小时前
Ansible Ad-Hoc 命令基础实战(Linux 系统)
linux·服务器·数据库
@土豆2 小时前
混合云组网-基于公有云产品实现(非开源方法)
运维·网络·开源
yy_xzz2 小时前
【Linux开发】04Linux 线程的销毁
linux