嵌入式Linux应用开发系列⑥:多线程编程——pthread、互斥锁、条件变量与线程池实战

上一篇我们掌握了多进程协作的四种IPC机制。多进程虽好,但切换开销大、共享数据不便。本篇将进入多线程世界,用pthread库实现轻量级并发,并逐步构建一个可复用的线程池。所有代码均交叉编译在ARM开发板上验证通过。


一、引言

嵌入式设备通常只有一个CPU核心(或少数几个),如何同时处理传感器采集、网络通信、用户交互?多线程是答案。相比多进程,线程共享地址空间,数据交换零成本,创建和切换开销极小。

但共享内存也带来了竞争条件的陷阱。本文将从线程创建开始,逐步引入互斥锁、条件变量,最终实现一个生产可用的线程池,让你彻底掌握Linux应用层多线程编程。


二、线程 vs 进程

特性 进程 线程
地址空间 独立 共享(除栈、寄存器等)
通信方式 IPC(管道、共享内存等) 直接读写全局变量
创建/切换开销
健壮性 一个崩溃不影响其他 一个崩溃可能导致整个进程终止
身份标识 PID TID(线程ID)

Linux内核中,线程和进程都用task_struct表示,通过clone系统调用创建。pthread库是用户态的线程标准。


三、核心API精讲

3.1 线程创建与终止

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

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
void pthread_exit(void *retval);
int pthread_join(pthread_t thread, void **retval);
int pthread_detach(pthread_t thread);

参数说明

  • thread:传出线程ID
  • attr:线程属性,通常传NULL使用默认属性
  • start_routine:线程入口函数,原型为void* func(void *arg)
  • arg:传递给入口函数的参数
  • pthread_exit:在线程内调用,终止当前线程
  • pthread_join:阻塞等待指定线程结束,回收资源(类似于wait
  • pthread_detach:分离线程,结束后自动回收资源,无需join

重要 :链接时必须加上-lpthread

3.2 互斥锁(Mutex)

c 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  // 静态初始化

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 互斥锁保证同一时刻只有一个线程进入临界区
  • 忘记解锁会导致死锁,务必配对使用

3.3 条件变量(Condition Variable)

条件变量用于线程间的等待-通知机制,必须配合互斥锁使用。

c 复制代码
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  // 静态初始化

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);       // 唤醒一个等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);    // 唤醒所有等待线程

使用范式

c 复制代码
// 等待端
pthread_mutex_lock(&mutex);
while (条件不满足) {
    pthread_cond_wait(&cond, &mutex);  // 原子地解锁并等待,被唤醒后重新加锁
}
// 条件满足,执行操作
pthread_mutex_unlock(&mutex);

// 通知端
pthread_mutex_lock(&mutex);
// 修改条件
pthread_cond_signal(&cond);  // 或 broadcast
pthread_mutex_unlock(&mutex);

为什么用while而非if 防止虚假唤醒(spurious wakeup)------线程可能因信号或调度被唤醒,但条件并未真正满足。


四、实战案例

实验1:线程创建与参数传递

c 复制代码
/**
 * thread_create_demo.c ------ 演示线程创建、传参与join回收
 * 编译: arm-linux-gnueabihf-gcc -Wall -g -o thread_create_demo thread_create_demo.c -lpthread
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

/* 线程入口函数 */
void *worker(void *arg)
{
    int num = *(int *)arg;
    printf("[线程 %ld] 收到参数: %d\n", (long)pthread_self(), num);
    sleep(1);
    int *result = malloc(sizeof(int));
    *result = num * num;   // 返回参数的平方
    return (void *)result;
}

int main(void)
{
    pthread_t tid[3];
    int args[3] = {1, 2, 3};
    int *retval;

    /* 创建3个线程 */
    for (int i = 0; i < 3; i++) {
        if (pthread_create(&tid[i], NULL, worker, &args[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
        printf("[主线程] 创建线程 %ld\n", (long)tid[i]);
    }

    /* 等待所有线程结束并获取返回值 */
    for (int i = 0; i < 3; i++) {
        pthread_join(tid[i], (void **)&retval);
        printf("[主线程] 线程 %ld 返回: %d\n", (long)tid[i], *retval);
        free(retval);
    }

    printf("[主线程] 全部线程结束\n");
    return 0;
}

预期输出(顺序可能不同):

复制代码
[主线程] 创建线程 ...
[线程 ...] 收到参数: 1
[线程 ...] 收到参数: 2
[线程 ...] 收到参数: 3
[主线程] 线程 ... 返回: 1
[主线程] 线程 ... 返回: 4
[主线程] 线程 ... 返回: 9
[主线程] 全部线程结束

实验2:互斥锁保护全局计数器

c 复制代码
/**
 * mutex_demo.c ------ 互斥锁保护共享资源
 * 编译: arm-linux-gnueabihf-gcc -Wall -g -o mutex_demo mutex_demo.c -lpthread
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

#define THREADS 10
#define ITERATIONS 100000

int counter = 0;                // 共享资源
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

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

int main(void)
{
    pthread_t threads[THREADS];

    /* 创建10个线程,每个自增100000次 */
    for (int i = 0; i < THREADS; i++) {
        pthread_create(&threads[i], NULL, increment, NULL);
    }

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

    printf("预期: %d, 实际: %d\n", THREADS * ITERATIONS, counter);
    pthread_mutex_destroy(&lock);
    return 0;
}

预期结果实际: 1000000。如果注释掉pthread_mutex_lock/unlock,结果将是不确定的错误值,这就是竞争条件。


实验3:生产者-消费者(条件变量)

c 复制代码
/**
 * producer_consumer_demo.c ------ 生产者-消费者模型
 * 编译: arm-linux-gnueabihf-gcc -Wall -g -o producer_consumer_demo producer_consumer_demo.c -lpthread
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];
int count = 0;  // 缓冲区当前数据数量

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t  cond_producer = PTHREAD_COND_INITIALIZER;  // 缓冲区不满时通知生产者
pthread_cond_t  cond_consumer = PTHREAD_COND_INITIALIZER;  // 缓冲区不空时通知消费者

void *producer(void *arg)
{
    for (int i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);
        while (count == BUFFER_SIZE) {  // 缓冲区满,等待
            pthread_cond_wait(&cond_producer, &mutex);
        }
        buffer[count] = i;
        printf("[生产者] 放入: %d, 缓冲区数量: %d\n", i, count + 1);
        count++;
        pthread_cond_signal(&cond_consumer);  // 通知消费者
        pthread_mutex_unlock(&mutex);
        sleep(1);  // 模拟生产耗时
    }
    return NULL;
}

void *consumer(void *arg)
{
    for (int i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);
        while (count == 0) {  // 缓冲区空,等待
            pthread_cond_wait(&cond_consumer, &mutex);
        }
        count--;
        int item = buffer[count];
        printf("[消费者] 取出: %d, 缓冲区数量: %d\n", item, count);
        pthread_cond_signal(&cond_producer);  // 通知生产者
        pthread_mutex_unlock(&mutex);
        sleep(1);  // 模拟消费耗时
    }
    return NULL;
}

int main(void)
{
    pthread_t prod, cons;

    pthread_create(&prod, NULL, producer, NULL);
    pthread_create(&cons, NULL, consumer, NULL);

    pthread_join(prod, NULL);
    pthread_join(cons, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond_producer);
    pthread_cond_destroy(&cond_consumer);

    printf("生产者-消费者演示结束\n");
    return 0;
}

实验4:简易线程池(C语言实现)

线程池是一种预创建一组线程,重复执行任务队列中任务的并发模型,避免了频繁创建销毁线程的开销。

c 复制代码
/**
 * threadpool.c ------ 简易线程池实现
 * 编译: arm-linux-gnueabihf-gcc -Wall -g -o threadpool_demo threadpool.c -lpthread
 *
 * 线程池结构:一个任务队列、一组工作线程、互斥锁与条件变量
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

/* ============ 任务节点 ============ */
typedef struct task {
    void (*function)(void *arg);
    void *arg;
    struct task *next;
} task_t;

/* ============ 线程池结构 ============ */
typedef struct {
    task_t        *task_head;      // 任务队列头
    task_t        *task_tail;      // 任务队列尾
    pthread_mutex_t mutex;          // 互斥锁
    pthread_cond_t  cond;           // 条件变量(有新任务或销毁时发信号)
    pthread_t     *threads;        // 工作线程数组
    int            thread_count;   // 线程数
    int            shutdown;       // 销毁标志
} threadpool_t;

/* 工作线程函数 */
static void *worker_thread(void *arg)
{
    threadpool_t *pool = (threadpool_t *)arg;

    while (1) {
        pthread_mutex_lock(&pool->mutex);

        /* 等待任务到来或销毁信号(使用 while 防虚假唤醒) */
        while (pool->task_head == NULL && !pool->shutdown) {
            pthread_cond_wait(&pool->cond, &pool->mutex);
        }

        if (pool->shutdown && pool->task_head == NULL) {
            pthread_mutex_unlock(&pool->mutex);
            break;
        }

        /* 取出队头任务 */
        task_t *task = pool->task_head;
        pool->task_head = task->next;
        if (pool->task_head == NULL)
            pool->task_tail = NULL;

        pthread_mutex_unlock(&pool->mutex);

        /* 执行任务 */
        task->function(task->arg);
        free(task);
    }
    return NULL;
}

/* 创建线程池 */
threadpool_t *threadpool_create(int num_threads)
{
    threadpool_t *pool = malloc(sizeof(threadpool_t));
    if (pool == NULL) return NULL;

    pool->task_head = NULL;
    pool->task_tail = NULL;
    pool->shutdown = 0;
    pool->thread_count = num_threads;

    pthread_mutex_init(&pool->mutex, NULL);
    pthread_cond_init(&pool->cond, NULL);

    pool->threads = malloc(sizeof(pthread_t) * num_threads);
    for (int i = 0; i < num_threads; i++) {
        pthread_create(&pool->threads[i], NULL, worker_thread, pool);
    }

    return pool;
}

/* 添加任务到线程池 */
int threadpool_add_task(threadpool_t *pool, void (*function)(void *), void *arg)
{
    task_t *task = malloc(sizeof(task_t));
    if (task == NULL) return -1;

    task->function = function;
    task->arg = arg;
    task->next = NULL;

    pthread_mutex_lock(&pool->mutex);
    if (pool->task_head == NULL) {
        pool->task_head = task;
        pool->task_tail = task;
    } else {
        pool->task_tail->next = task;
        pool->task_tail = task;
    }
    pthread_cond_signal(&pool->cond);  // 通知一个等待线程
    pthread_mutex_unlock(&pool->mutex);

    return 0;
}

/* 销毁线程池 */
void threadpool_destroy(threadpool_t *pool)
{
    pthread_mutex_lock(&pool->mutex);
    pool->shutdown = 1;
    pthread_cond_broadcast(&pool->cond);  // 唤醒所有线程
    pthread_mutex_unlock(&pool->mutex);

    /* 等待所有线程退出 */
    for (int i = 0; i < pool->thread_count; i++) {
        pthread_join(pool->threads[i], NULL);
    }

    free(pool->threads);
    pthread_mutex_destroy(&pool->mutex);
    pthread_cond_destroy(&pool->cond);
    free(pool);
}

/* ============ 测试用例 ============ */
void sample_task(void *arg)
{
    int id = *(int *)arg;
    printf("[任务 %d] 由线程 %ld 执行\n", id, (long)pthread_self());
    usleep(100000);  // 模拟工作耗时
}

int main(void)
{
    /* 创建一个4线程的线程池 */
    threadpool_t *pool = threadpool_create(4);

    /* 添加10个任务 */
    int ids[10];
    for (int i = 0; i < 10; i++) {
        ids[i] = i + 1;
        threadpool_add_task(pool, sample_task, &ids[i]);
    }

    /* 等待一段时间后销毁(销毁时会处理队列中剩余任务) */
    sleep(2);
    threadpool_destroy(pool);

    printf("线程池演示结束\n");
    return 0;
}

交叉编译与测试

bash 复制代码
arm-linux-gnueabihf-gcc -Wall -g -o threadpool_demo threadpool.c -lpthread
scp threadpool_demo root@192.168.1.100:/root/
./threadpool_demo

五、多线程编程陷阱与最佳实践

陷阱 现象 对策
数据竞争(无锁保护) 结果不确定,偶发崩溃 用互斥锁保护共享数据
死锁(多锁顺序不一致) 程序卡死 统一加锁顺序,或使用pthread_mutex_trylock
忘记解锁 其他线程永久阻塞 使用goto__attribute__((cleanup))确保解锁
虚假唤醒 条件变量被意外唤醒 始终用while而非if检查条件
线程泄漏(未join/detach) 资源无法回收 joindetach,或使用线程池
信号处理函数中使用锁 死锁 信号处理函数中禁止使用锁
fork后多线程状态不一致 子进程只剩调用fork的线程 多线程程序fork后子进程应立即exec

六、总结与思考题

本篇系统讲解了Linux多线程编程的核心:pthread创建、互斥锁保护、条件变量同步,并最终实现了一个简易但完整的线程池。

核心要点

  1. 线程共享地址空间,通信直接但需同步
  2. 互斥锁保护临界区,条件变量实现等待-通知
  3. 条件变量必须配合while检查条件,防止虚假唤醒
  4. 线程池预先创建线程,任务入队后由空闲线程执行
  5. 编译链接务必加上-lpthread

思考题

  1. 如果生产者-消费者示例中只有一个条件变量(生产者消费者共用),会有什么问题?请尝试修改代码验证。
  2. 线程池示例中,如果一个任务运行很久,会不会阻塞其他任务?如何改进以支持任务优先级?
  3. 多线程程序中调用fork,子进程继承了多少线程?可能导致什么问题?

欢迎在评论区留下你的思考。下一篇我们将踏入网络编程------Socket、select/poll/epoll与高并发服务器的核心地带。


参考资料

  • 《UNIX环境高级编程(APUE)》第11、12章 Threads
  • man pthread_create, man pthread_mutex_init, man pthread_cond_wait
  • POSIX Threads Programming (Blaise Barney, LLNL)