第30章 多线程编程入门:pthread_create()、pthread_join()、pthread_exit()、线程同步(互斥锁pthread_mutex_t)

终于到了多线程这一章。说实话,很多嵌入式工程师对多线程又爱又怕。爱的是它能榨干CPU每一滴性能,怕的是那些莫名其妙的数据错乱和死锁。我刚开始接触pthread时,也踩过不少坑。今天咱们就把这几个核心API和互斥锁彻底讲透。

30.1 线程是什么?为什么需要它?

一个进程就像一家工厂,线程就是工厂里的工人。单线程程序,相当于只有一个工人在干活。多线程,就是多个工人同时工作。你想想看,如果只有一个工人,他既要搬砖又要记账,效率肯定上不去。

在嵌入式系统里,我经常用多线程来处理这样的场景:一个线程负责从传感器读取数据,另一个线程负责把数据通过串口发出去,第三个线程负责响应用户按键。各干各的,互不干扰。

核心要点:线程是CPU调度的最小单位。同一进程内的线程共享地址空间、文件描述符等资源,但各自拥有独立的栈和寄存器上下文。

30.2 三个核心API:创建、等待、退出

30.2.1 pthread_create() —— 创建线程

这个函数是咱们的起点。原型长这样:

#include <pthread.h>

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

参数解释一下:

  • thread:指向pthread_t变量的指针,用来接收线程ID
  • attr:线程属性,传NULL就用默认属性
  • start_routine:线程要执行的函数,返回void*,参数也是void*
  • arg:传给线程函数的参数

返回值:成功返回0,失败返回错误码。

我的习惯:每次调用pthread_create后,一定检查返回值。别偷懒,否则线程创建失败了你都不知道,程序静悄悄地挂掉。

看个简单例子:

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

void* thread_func(void* arg) {
    int* num = (int*)arg;
    printf("子线程收到参数: %d\n", *num);
    return NULL;
}

int main() {
    pthread_t tid;
    int value = 42;

    if (pthread_create(&tid, NULL, thread_func, &value) != 0) {
        perror("pthread_create failed");
        return 1;
    }

    printf("主线程继续执行...\n");

    pthread_join(tid, NULL);
    return 0;
}

30.2.2 pthread_join() —— 等待线程结束

主线程创建子线程后,如果不做任何处理,主线程可能先跑完退出,子线程还没跑完就被强制终止了。pthread_join就是用来解决这个问题的——它会让调用者阻塞,直到目标线程退出。

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

第二个参数retval用来接收线程的返回值。如果不需要,传NULL即可。

我曾经踩过的坑:对同一个线程调用两次pthread_join,会导致未定义行为,程序可能直接崩溃。记住,一个线程只能被join一次。

30.2.3 pthread_exit() —— 线程主动退出

线程可以在函数末尾return,也可以调用pthread_exit主动退出。两者效果类似,但pthread_exit可以在函数任意位置调用,而且它不会导致整个进程退出(exit()会)。

void pthread_exit(void *retval);

看个综合例子:

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

void* worker(void* arg) {
    int id = *(int*)arg;
    printf("线程 %d 开始工作\n", id);

    // 模拟一些计算
    int result = id * 100;
    
    printf("线程 %d 结束,结果: %d\n", id, result);
    pthread_exit((void*)(long)result);
}

int main() {
    pthread_t t1, t2;
    int id1 = 1, id2 = 2;
    void* ret1, *ret2;

    pthread_create(&t1, NULL, worker, &id1);
    pthread_create(&t2, NULL, worker, &id2);

    pthread_join(t1, &ret1);
    pthread_join(t2, &ret2);

    printf("主线程: t1返回 %ld, t2返回 %ld\n", 
           (long)ret1, (long)ret2);
    return 0;
}

30.3 线程同步:为什么需要互斥锁?

多个线程共享全局变量时,问题就来了。假设两个线程同时对一个全局计数器做自增操作:

int counter = 0;

// 线程A
counter++;  // 实际是三步:读-改-写

// 线程B
counter++;  // 同样三步

如果线程A刚读完counter的值(比如0),还没来得及写回,线程B也读到了0,然后各自加1写回,结果counter变成1而不是2。这就是经典的竞争条件。

我在项目中遇到过类似问题:一个多线程数据采集系统,两个线程同时往环形缓冲区写数据,结果数据覆盖了,采集到的波形全是乱的。查了两天才定位到是缺少互斥保护。

30.4 互斥锁 pthread_mutex_t

互斥锁,说白了就是一把锁。线程在访问共享资源前先上锁,用完再解锁。其他线程想上锁时,如果锁已被占用,就会阻塞等待。

30.4.1 基本用法

#include <pthread.h>

// 定义互斥锁变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 或者动态初始化
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

// 上锁
pthread_mutex_lock(&mutex);

// 访问共享资源...

// 解锁
pthread_mutex_unlock(&mutex);

// 销毁锁
pthread_mutex_destroy(&mutex);

重要原则:锁的粒度要适中。锁得太细,保护不全面;锁得太粗,性能下降严重。我一般遵循「保护最小必要范围」的原则。

30.4.2 完整示例:用互斥锁保护计数器

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

#define NUM_THREADS 5
#define LOOP_COUNT 1000000

int counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* increment(void* arg) {
    for (int i = 0; i < LOOP_COUNT; i++) {
        pthread_mutex_lock(&mutex);
        counter++;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];

    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_create(&threads[i], NULL, increment, NULL);
    }

    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    printf("最终计数: %d (期望: %d)\n", 
           counter, NUM_THREADS * LOOP_COUNT);

    pthread_mutex_destroy(&mutex);
    return 0;
}

运行结果:

最终计数: 5000000 (期望: 5000000)

如果不加锁,结果大概率小于5000000,而且每次运行都不一样。

30.5 死锁:最头疼的问题

死锁就是两个线程互相等待对方释放锁,结果谁也动不了。典型的场景:线程A持有锁1,等待锁2;线程B持有锁2,等待锁1。

我曾经遇到过一次:一个多线程日志系统,写日志线程和刷盘线程互相等待,整个系统卡死。最后发现是锁的获取顺序不一致导致的。从那以后,我强制团队规定:所有线程获取多个锁时,必须按照相同的顺序。

避免死锁的几个原则:

  • 固定锁的获取顺序(比如总是先锁A再锁B)
  • 使用pthread_mutex_trylock()尝试加锁,失败就释放已持有的锁
  • 尽量减小锁的持有时间

30.6 本章知识体系

下面这张图帮你理清多线程编程的核心脉络:

多线程编程核心知识体系 线程生命周期管理 pthread_create() pthread_join() pthread_exit() 线程同步机制 互斥锁 pthread_mutex_t 死锁预防 竞争条件避免 最佳实践:固定锁顺序 · 最小锁粒度 · 检查返回值

30.7 避坑指南与个人经验

最后,分享几个我多年积累的经验:

  • 检查返回值:pthread系列函数返回错误码,不是设置errno。别用perror,用strerror打印错误信息。
  • 线程栈大小:嵌入式系统内存有限,默认线程栈可能太大。用pthread_attr_setstacksize()调整。
  • 不要用锁保护太小的操作:比如counter++这种,用原子操作__sync_fetch_and_add()更高效。
  • 调试多线程程序:gdb支持多线程调试,用info threads查看所有线程,thread n切换线程。

我的建议:刚开始学多线程,先写简单的生产者-消费者模型。一个线程生产数据,一个线程消费数据,用互斥锁保护缓冲区。这个模型搞懂了,多线程就算入门了。

多线程编程就像走钢丝,平衡好了性能飞升,平衡不好bug丛生。但别怕,多写多练,慢慢就能掌握其中的门道。


公众号:蓝海资料掘金营,微信deep3321