模块化编程中的多线程模块
多线程,说白了就是让程序同时干好几件事。我刚开始接触嵌入式多线程时,总觉得这东西是Linux上的专利。后来做了一款带网络交互的工业控制器,才意识到——嗯,裸机也能跑多线程,关键看你怎么封装。
今天咱们聊聊多线程模块的四个核心:线程封装、互斥锁与条件变量、线程池设计、原子操作。这些内容,我建议你当成工具箱里的常备件。
一、线程封装:别让OS细节污染你的业务代码
我在项目中遇到过最头疼的事:代码里到处是pthread_create、pthread_join,换一个RTOS就得重写一遍。后来我学乖了——做个抽象层。
先看一个简单的线程封装接口:
// thread.h
typedef void* (*thread_func_t)(void* arg);
typedef struct {
void* handle; // 平台相关句柄
thread_func_t func;
void* arg;
char name[32];
int priority;
size_t stack_size;
} thread_t;
int thread_create(thread_t* t);
int thread_join(thread_t* t);
void thread_sleep(int ms);
这个结构体把线程的“身份证”都装进去了。名字、优先级、栈大小,这些在调试时特别有用。我记得有一次排查死锁,就是靠线程名字定位到具体任务的。
底层实现根据不同平台切换:
// thread_pthread.c
int thread_create(thread_t* t) {
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, t->stack_size);
return pthread_create(
(pthread_t*)&t->handle,
&attr,
t->func,
t->arg
);
}
你想想看,如果项目从Linux迁移到FreeRTOS,只需要换一个.c文件,上层业务代码完全不用动。这就是模块化的价值。
二、互斥锁与条件变量:别让数据打架
多线程最怕什么?数据竞争。两个线程同时写一个变量,结果不可预测。互斥锁就是解决这个问题的。
但光有锁还不够。有时候线程需要等某个条件成立再继续,比如生产者-消费者模型。这时候条件变量就派上用场了。
看一个封装好的等待队列:
// sync.h
typedef struct {
void* mutex;
void* cond;
int signaled;
} event_t;
int event_init(event_t* ev);
int event_wait(event_t* ev, int timeout_ms);
int event_signal(event_t* ev);
int event_broadcast(event_t* ev);
实现时要注意一点:条件变量必须和互斥锁配合使用。我曾经犯过一个错——先解锁再等待条件变量,结果信号在等待之前就发出了,线程永远等不到。
// 正确用法
int event_wait(event_t* ev, int timeout_ms) {
pthread_mutex_lock(ev->mutex);
while (!ev->signaled) {
// 这里用while而不是if,防止虚假唤醒
pthread_cond_timedwait(ev->cond, ev->mutex, &ts);
}
ev->signaled = 0;
pthread_mutex_unlock(ev->mutex);
return 0;
}
三、线程池设计:别频繁创建销毁线程
频繁创建线程开销很大。我记得一个网络服务项目,每来一个请求就创建一个线程,结果系统负载一高,线程数飙到几百,上下文切换把CPU吃光了。
线程池的思路很简单:提前创建一批线程,任务来了就丢给空闲线程处理。
核心结构:
typedef struct task_s {
void (*func)(void* arg);
void* arg;
struct task_s* next;
} task_t;
typedef struct {
task_t* head;
task_t* tail;
int count;
pthread_mutex_t lock;
pthread_cond_t not_empty;
} task_queue_t;
typedef struct {
pthread_t* threads;
int thread_count;
task_queue_t queue;
int shutdown;
} thread_pool_t;
线程池的工作流程:
// 工作线程的循环
void* worker_loop(void* arg) {
thread_pool_t* pool = (thread_pool_t*)arg;
while (1) {
pthread_mutex_lock(&pool->queue.lock);
while (pool->queue.count == 0 && !pool->shutdown) {
pthread_cond_wait(&pool->queue.not_empty, &pool->queue.lock);
}
if (pool->shutdown) {
pthread_mutex_unlock(&pool->queue.lock);
break;
}
// 取出任务
task_t* task = pool->queue.head;
pool->queue.head = task->next;
pool->queue.count--;
pthread_mutex_unlock(&pool->queue.lock);
// 执行任务(注意:这里已经释放了锁)
task->func(task->arg);
free(task);
}
return NULL;
}
这里有个细节:执行任务时要释放锁。如果带着锁执行任务,其他线程连取任务都取不了,等于串行化了。
四、原子操作:无锁编程的基石
有些场景下,加锁太重了。比如一个计数器,多个线程同时加1。用互斥锁?太浪费。这时候原子操作就登场了。
C11标准提供了
// atomic.h
#if defined(__GNUC__) || defined(__clang__)
#define atomic_add(ptr, val) __sync_fetch_and_add(ptr, val)
#define atomic_sub(ptr, val) __sync_fetch_and_sub(ptr, val)
#define atomic_inc(ptr) __sync_fetch_and_add(ptr, 1)
#define atomic_dec(ptr) __sync_fetch_and_sub(ptr, 1)
#define atomic_cas(ptr, old, new) __sync_bool_compare_and_swap(ptr, old, new)
#elif defined(_MSC_VER)
#include <intrin.h>
#define atomic_add(ptr, val) InterlockedExchangeAdd(ptr, val)
// ... 其他平台适配
#endif
原子操作最经典的应用是引用计数:
typedef struct {
atomic_int refcount;
// ... 其他数据
} shared_data_t;
void ref(shared_data_t* data) {
atomic_inc(&data->refcount);
}
void unref(shared_data_t* data) {
if (atomic_dec(&data->refcount) == 1) {
// 最后一个引用释放,可以安全销毁
free(data);
}
}
注意:原子操作只能保证单个变量的读写是原子的。如果你需要多个变量保持一致状态,还是得用锁。
知识体系总览
下面这张图把多线程模块的核心关系梳理清楚了:
这张图把四个模块的关系和子内容都串起来了。你可以看到,线程池依赖互斥锁和条件变量,而原子操作是独立的基础工具。实际项目中,这四个模块经常组合使用。
好了,多线程模块就聊到这儿。记住:封装是为了复用,复用是为了少踩坑。下次写多线程代码时,试试把这些模块拿出来直接用,你会发现世界清净很多。
公众号:蓝海资料掘金营,微信deep3321