Linux多线程编程完全指南:线程同步、互斥锁与生产者消费者模型
引言在上一篇文章中我们学习了线程的创建、退出和等待机制并发现了多线程并发访问共享变量时的竞态条件问题。我们使用互斥锁解决了这个问题。今天我们将在此基础上深入探讨线程同步的经典问题——生产者消费者模型并全面回顾进程与线程的区别、线程的实现方式等核心概念。第一部分上节回顾——信号量与互斥锁一、信号量信号量是用于进程/线程同步的机制核心操作包括操作函数作用初始化sem_init()初始化信号量设置初始值P操作sem_wait()信号量值减1为0时阻塞V操作sem_post()信号量值加1唤醒等待线程销毁sem_destroy()销毁信号量#include semaphore.h // 初始化信号量 sem_t sem; sem_init(sem, 0, 1); // 第二个参数0表示线程间共享 // P操作申请资源 sem_wait(sem); // V操作释放资源 sem_post(sem); // 销毁 sem_destroy(sem);二、互斥锁Mutex互斥锁是专门用于实现互斥访问的同步机制与初值为1的信号量功能等价。操作函数作用初始化pthread_mutex_init初始化互斥锁加锁pthread_mutex_lock锁被占用时阻塞解锁pthread_mutex_unlock释放锁销毁pthread_mutex_destroy销毁互斥锁#include pthread.h pthread_mutex_t mutex; // 初始化 pthread_mutex_init(mutex, NULL); // 加锁 pthread_mutex_lock(mutex); // 解锁 pthread_mutex_unlock(mutex); // 销毁 pthread_mutex_destroy(mutex);三、互斥锁与信号量的关系特性互斥锁信号量初值1本质二进制锁计数器操作lock/unlockP/V所有权只有加锁线程能解锁任何线程都可V操作适用场景保护临界区资源计数、同步第二部分进程与线程的区别一、基本概念概念定义特点进程正在运行的程序资源分配的基本单位进程间相互隔离独立的内存空间线程进程内部的执行路径CPU调度的基本单位同一进程的线程共享内存空间核心区别不同进程之间的内存空间是隔离的一个进程无法直接访问另一个进程的内存同一进程内的多个线程共享内存空间一个线程修改的变量其他线程可以看到#include stdio.h #include pthread.h #include unistd.h int shared_var 0; // 全局变量线程间共享 void* thread_func(void* arg) { shared_var 100; printf(子线程: shared_var %d\n, shared_var); return NULL; } int main() { pthread_t tid; pthread_create(tid, NULL, thread_func, NULL); pthread_join(tid, NULL); printf(主线程: shared_var %d\n, shared_var); // 输出100 return 0; }二、查看进程和线程ID#include stdio.h #include pthread.h #include unistd.h void* thread_func(void* arg) { printf(子线程: PID%d, TID%lu\n, getpid(), pthread_self()); return NULL; } int main() { pthread_t tid; printf(主线程: PID%d, TID%lu\n, getpid(), pthread_self()); pthread_create(tid, NULL, thread_func, NULL); pthread_join(tid, NULL); return 0; }使用命令行查看线程# 查看进程及其线程ps -eLf | grep program_name# 或使用 -T 选项ps -T -p [PID]三、线程的实现方式实现方式特点优缺点用户级线程用户空间管理内核只看到一条执行路径创建快但无法利用多核内核级线程内核直接管理内核可见多条路径可利用多核Linux采用此方式组合模型用户级和内核级结合兼顾灵活性和性能Linux内核的独特视角Linux内核没有单独的线程概念线程被视作与其他进程共享资源的进程每个线程都有独立的task_struct进程描述符通过共享内存空间、文件描述符等实现线程特性第三部分生产者消费者模型一、问题描述生产者消费者模型Producer-Consumer Problem是操作系统的经典同步问题生产者向缓冲区中写入数据消费者从缓冲区中读取数据缓冲区有限大小的共享区域二、同步条件条件说明控制方式互斥访问同一时刻只能一个线程操作缓冲区互斥锁缓冲区非满满时生产者不能写入信号量empty缓冲区非空空时消费者不能读取信号量full三、同步逻辑设计重要原则先同步后互斥即先执行P操作同步判断再加锁互斥访问避免死锁。四、代码实现头文件与定义#include stdio.h #include stdlib.h #include pthread.h #include semaphore.h #include unistd.h #include time.h #define BUFFER_SIZE 30 // 缓冲区大小 #define PRODUCER_NUM 2 // 生产者数量 #define CONSUMER_NUM 3 // 消费者数量 #define PRODUCE_COUNT 30 // 每个生产者生产数量 #define CONSUME_COUNT 20 // 每个消费者消费数量 int buffer[BUFFER_SIZE]; // 缓冲区 int in 0; // 生产者写入位置 int out 0; // 消费者读取位置 sem_t empty; // 空闲格子数生产者用 sem_t full; // 满格子数消费者用 pthread_mutex_t mutex; // 互斥锁初始化void init() { // 初始化信号量 sem_init(empty, 0, BUFFER_SIZE); // 初始有BUFFER_SIZE个空闲 sem_init(full, 0, 0); // 初始没有数据 // 初始化互斥锁 pthread_mutex_init(mutex, NULL); // 随机数种子 srand(time(NULL)); }生产者函数void* producer(void* arg) { for (int i 0; i PRODUCE_COUNT; i) { // 1. 等待空闲格子 sem_wait(empty); // 2. 加锁 pthread_mutex_lock(mutex); // 3. 生产数据 int data rand() % 100; buffer[in] data; printf(生产者[%lu] 写入位置[%d]: %d\n, pthread_self(), in, data); // 4. 更新写入位置 in (in 1) % BUFFER_SIZE; // 5. 解锁 pthread_mutex_unlock(mutex); // 6. 通知消费者 sem_post(full); // 模拟生产耗时 usleep(rand() % 100000); } return NULL; }消费者函数void* consumer(void* arg) { for (int i 0; i CONSUME_COUNT; i) { // 1. 等待有数据 sem_wait(full); // 2. 加锁 pthread_mutex_lock(mutex); // 3. 消费数据 int data buffer[out]; printf(消费者[%lu] 读取位置[%d]: %d\n, pthread_self(), out, data); // 4. 更新读取位置 out (out 1) % BUFFER_SIZE; // 5. 解锁 pthread_mutex_unlock(mutex); // 6. 通知生产者有空闲格子 sem_post(empty); // 模拟消费耗时 usleep(rand() % 100000); } return NULL; }主函数int main() { pthread_t producers[PRODUCER_NUM]; pthread_t consumers[CONSUMER_NUM]; init(); // 创建生产者线程 for (int i 0; i PRODUCER_NUM; i) { pthread_create(producers[i], NULL, producer, NULL); } // 创建消费者线程 for (int i 0; i CONSUMER_NUM; i) { pthread_create(consumers[i], NULL, consumer, NULL); } // 等待生产者结束 for (int i 0; i PRODUCER_NUM; i) { pthread_join(producers[i], NULL); } // 等待消费者结束 for (int i 0; i CONSUMER_NUM; i) { pthread_join(consumers[i], NULL); } // 销毁资源 sem_destroy(empty); sem_destroy(full); pthread_mutex_destroy(mutex); return 0; }五、为什么先同步后互斥// ❌ 错误顺序先加锁后同步 pthread_mutex_lock(mutex); sem_wait(empty); // 如果缓冲区满这里会阻塞 // 此时锁仍然被持有消费者无法进入 // 如果消费者也无法进入形成死锁 // ✅ 正确顺序先同步后加锁 sem_wait(empty); // 先判断是否能操作 pthread_mutex_lock(mutex); // 再独占缓冲区 // 操作... pthread_mutex_unlock(mutex); sem_post(full);第四部分死循环与有限次循环一、两种模式对比模式特点适用场景有限次循环生产/消费固定数量后结束批量处理任务死循环持续生产/消费永不停止服务器、守护进程// 有限次循环 for (int i 0; i PRODUCE_COUNT; i) { // 生产数据 } // 死循环 while (1) { // 生产数据 }二、死循环版本的特点生产者持续生产消费者持续消费程序永远不会主动退出需CtrlC或kill终止常用于服务端程序、消息队列等场景总结一、互斥锁与信号量总结特性互斥锁信号量初始化pthread_mutex_initsem_init加锁/Ppthread_mutex_locksem_wait解锁/Vpthread_mutex_unlocksem_post有所有权✅ 只有加锁才能解锁❌ 任何线程都可V适用互斥访问资源计数、同步二、生产者消费者模型核心要点要点说明同步条件缓冲区非满生产者、非空消费者互斥条件同一时刻只有一个线程操作缓冲区信号量empty初始为缓冲区大小控制生产者信号量full初始为0控制消费者操作顺序先同步P操作后互斥加锁三、进程与线程总结维度进程线程资源分配独立空间共享空间通信方式IPC管道、共享内存等直接访问共享变量创建开销大小切换开销大小Linux中的本质task_structtask_struct共享资源本文是Linux多线程编程系列的下篇重点讲解了互斥锁的基本使用和注意事项进程与线程的区别及Linux内核的实现方式生产者消费者模型的同步逻辑和代码实现先同步后互斥原则的重要性面试高频考点生产者消费者模型的代码实现和同步逻辑描述互斥锁与信号量的区别进程与线程的区别为什么需要先同步后互斥学习建议理解生产者消费者模型后再看代码不要死记硬背动手运行代码观察生产者和消费者的执行顺序尝试修改生产者/消费者数量观察效果将有限次循环改为死循环理解两种模式的区别