最近在调一个多线程后台服务,生产者和消费者之间的数据交换一开始是用“共享结构体+锁”的方式硬扛,结果线程数一上来,代码越来越别扭:锁竞争、偶发崩溃、调度混乱全来了。折腾了一周之后,我决定把线程间数据交换统一收敛成线程间消息队列模型,问题顿时清爽了很多。今天把这段能在生产环境稳定跑的 Linux 线程间消息队列代码整理出来,算是《Linux实用功能代码集》的第三篇。
这篇会用 C 语言 + pthread 实现一个基础但完整的线程间消息队列,包含阻塞发送、阻塞接收、非阻塞发送、超时接收这几个最常用的接口,再配合一个多生产者多消费者的完整例子。看完之后你可以直接把这套代码抄到自己的项目里,也能理解为什么条件变量必须配互斥锁、为什么等待要写在 while 里而不是 if 里。适合正在做 Linux C/C++ 开发、嵌入式开发,或者准备系统编程面试的朋友。
1. 先搞清楚线程间消息队列到底解决什么问题
1.1 为什么“共享变量+锁”的方案越写越难受
很多人刚开始做多线程数据交换,第一反应是搞一个全局结构体,然后所有线程都拿同一把锁去保护它。这种做法在功能上没错,但实际工程里会迅速暴露出问题。
首先是耦合太紧。生产者线程要往结构体里写字段,消费者线程要读字段,两边的代码都必须清楚知道结构体内部长什么样。一旦业务逻辑变多,比如你要从网络线程往解析线程丢一段数据,解析线程又要给业务线程回传结果,结构体里字段会越来越多,锁的粒度也越搞越细,最后连加锁顺序都能成为死锁源头。
其次是高吞吐场景下锁竞争严重。一把大锁把所有线程的访问串行化,线程一多,大家都在抢锁,真正的业务处理时间反而被锁等待挤占。用一个简单比喻:消息队列相当于给每个线程发了一个专用信箱,生产线程只负责把信投进信箱,消费线程只负责从信箱取信,两边不需要知道对方的具体运作方式。而“共享变量+锁”是所有人挤在一张桌子上抢同一份文件,乱是必然的。
还有一个问题是轮询检测。有些开发者为了避免锁,让消费者循环检查一个标志位有没有被置位,发现置位了再处理数据。这在实时性和 CPU 开销上都很吃亏:轮询间隔短了 CPU 空转,间隔长了消息延迟变高。线程间消息队列通过条件变量的阻塞唤醒机制,让消费者在队列为空时安稳睡觉,有消息时被立刻叫醒,二者兼得。
1.2 一个线程间消息队列需要同时解决的核心问题
设计线程间消息队列,本质上要同时处理三件事。
第一是并发安全。队列本身是共享资源,所有操作必须用互斥锁保护,这个没什么好说的。
第二是阻塞与唤醒。消费者发现队列为空时不应该忙等,而是挂起;生产者放入消息后要能把消费者唤醒。反过来,队列满的时候生产者也要能挂起,等消费者取走消息腾出空间后再继续。这就是条件变量的典型应用场景。
第三是容量与背压。队列必须有上限,不能任凭生产者无限狂塞。固定容量队列一旦满了,生产者要么阻塞等待,要么选择丢弃或返回失败,这就形成了一种“背压”机制。很多人忽视这一点,结果生产速度一快,消息对象在内存里越堆越多,最后 OOM 了才来排查,非常痛苦。
数据结构上,我选择了环形数组而不是链表。原因有三个:数组的内存是预分配的,不需要每条消息额外 malloc 节点,避免了频繁堆分配;环形数组的头部和尾部索引移动就是简单的取模运算,实现起来不容易出错;实际运行中,数组的连续内存对缓存更友好,吞吐表现通常优于链表。链表队列适合消息大小差异离谱、需要频繁插入删除的场景,但作为通用线程间消息队列,环形数组是我的默认首选。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一版可直接抄作业的 C 代码实现
2.1 数据结构和接口设计
先定义队列对象。这里我把互斥锁、两个条件变量和环形数组索引全部封装在一个结构体里,使用方不需要关心内部实现。
c复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
typedef struct {
void **buf; // 消息指针数组
int capacity; // 队列容量
int head; // 队头,读取位置
int tail; // 队尾,写入位置
int count; // 当前消息数
pthread_mutex_t mutex;
pthread_cond_t not_full; // 队列未满
pthread_cond_t not_empty; // 队列非空
} msg_queue_t;
接口我保留了五个最常用的:
c复制int msgq_init(msg_queue_t *q, int capacity);
void msgq_destroy(msg_queue_t *q);
int msgq_send(msg_queue_t *q, void *msg); // 阻塞发送
int msgq_send_noblock(msg_queue_t *q, void *msg); // 非阻塞发送,满返回 -1
void *msgq_recv(msg_queue_t *q); // 阻塞接收
void *msgq_recv_noblock(msg_queue_t *q); // 非阻塞接收,空返回 NULL
int msgq_recv_timeout(msg_queue_t *q, void **msg, int timeout_ms); // 超时接收
注意消息的存储方式是存放 void * 指针,而不是拷贝整块数据。这样设计的好处是队列本身只负责搬移指针,非常轻量;缺点是需要调用双方约定好消息对象的内存生命周期。后面第三章我会专门展开讲这个约定,现在先看实现。
2.2 核心函数实现:发送、接收、超时接收
初始化时给环形数组分配空间,互斥锁和条件变量分别初始化。
c复制int msgq_init(msg_queue_t *q, int capacity) {
memset(q, 0, sizeof(*q));
q->buf = (void **)malloc(sizeof(void *) * capacity);
if (!q->buf)
return -1;
q->capacity = capacity;
pthread_mutex_init(&q->mutex, NULL);
pthread_c
