10. 结构体与多线程:结构体作为线程参数、线程安全的结构体设计
多线程编程,说白了就是让程序同时干几件事。但事情一多,就容易打架。结构体作为数据的“集装箱”,在多线程环境里怎么安全地传递和共享,是个绕不开的坎儿。我早年做嵌入式数据采集系统时,就吃过结构体共享的亏——两个线程同时写一个结构体字段,结果数据全乱了,查了三天才找到原因。
10.1 结构体作为线程参数
创建线程时,我们经常需要传一些参数进去。结构体是最自然的载体——把一堆相关数据打包,一次性传过去。
10.1.1 传值还是传指针?
这个问题我每次面试新人都会问。答案是:传指针。因为pthread_create的最后一个参数是void*,你传结构体本身进去,会被隐式转换成指针,编译器会报警告。而且传值意味着拷贝整个结构体,效率低。
// 正确的做法:传结构体指针
typedef struct {
int id;
char name[32];
float value;
} TaskParam;
void* worker(void* arg) {
TaskParam* param = (TaskParam*)arg;
printf("Task %d: %s, value=%.2f\n", param->id, param->name, param->value);
return NULL;
}
int main() {
pthread_t tid;
TaskParam param = {1, "sensor_read", 3.14};
pthread_create(&tid, NULL, worker, ¶m);
pthread_join(tid, NULL);
return 0;
}
10.1.2 动态分配更安全
我个人习惯用malloc分配结构体,然后传给线程。线程用完后自己free。这样生命周期完全由线程控制,不会出现主线程提前释放的问题。
void* worker(void* arg) {
TaskParam* param = (TaskParam*)arg;
// 处理数据...
free(param); // 线程自己释放
return NULL;
}
int main() {
pthread_t tid;
TaskParam* param = malloc(sizeof(TaskParam));
param->id = 1;
strcpy(param->name, "sensor_read");
param->value = 3.14;
pthread_create(&tid, NULL, worker, param);
pthread_detach(tid); // 分离线程,不等待
return 0;
}
10.2 线程安全的结构体设计
多个线程同时读写同一个结构体,就会出问题。你想想看,一个线程在读某个字段,另一个线程在写,读到的数据可能半新不旧——这就是数据竞争。
10.2.1 互斥锁保护
最直接的办法:给结构体加一把锁。每个操作前先上锁,操作完解锁。
typedef struct {
int x;
int y;
pthread_mutex_t lock;
} Point;
void point_set(Point* p, int x, int y) {
pthread_mutex_lock(&p->lock);
p->x = x;
p->y = y;
pthread_mutex_unlock(&p->lock);
}
void point_get(Point* p, int* x, int* y) {
pthread_mutex_lock(&p->lock);
*x = p->x;
*y = p->y;
pthread_mutex_unlock(&p->lock);
}
10.2.2 原子操作
如果只是读写一个int或指针,用原子操作比互斥锁轻量得多。C11标准提供了stdatomic.h,嵌入式里也常用GCC的__sync_系列。
#include <stdatomic.h>
typedef struct {
atomic_int counter;
atomic_flag flag;
} AtomicStruct;
void atomic_increment(AtomicStruct* as) {
atomic_fetch_add(&as->counter, 1);
}
bool atomic_test_flag(AtomicStruct* as) {
return atomic_flag_test_and_set(&as->flag);
}
原子操作的好处是不用加锁,性能好。但只能处理简单的读写,复杂的业务逻辑还是得用互斥锁。
10.2.3 读写锁优化
读多写少的场景,用读写锁更高效。多个线程可以同时读,但写的时候独占。
typedef struct {
int data[1024];
pthread_rwlock_t rwlock;
} SharedData;
void read_data(SharedData* sd, int index) {
pthread_rwlock_rdlock(&sd->rwlock);
int val = sd->data[index];
pthread_rwlock_unlock(&sd->rwlock);
// 使用val...
}
void write_data(SharedData* sd, int index, int val) {
pthread_rwlock_wrlock(&sd->rwlock);
sd->data[index] = val;
pthread_rwlock_unlock(&sd->rwlock);
}
10.3 避坑指南
我曾经在一个项目里,结构体里有个指针成员,指向动态分配的内存。两个线程共享这个结构体,一个线程释放了指针指向的内存,另一个线程还在用——段错误,程序直接崩了。从那以后,我总结了几条铁律:
- 不要返回结构体内部指针——如果必须返回,用引用计数管理生命周期。
- 结构体里的锁要初始化——用PTHREAD_MUTEX_INITIALIZER或pthread_mutex_init,别漏了。
- 拷贝结构体时要深拷贝——如果结构体里有指针成员,浅拷贝会导致多个线程共享同一块内存。
- 避免锁的嵌套——一个线程持有一个锁,再去申请另一个锁,很容易死锁。如果必须嵌套,保证所有线程的加锁顺序一致。
10.4 知识体系图
下面这张图总结了结构体与多线程的核心知识点,你看一眼就能理清脉络:
10.5 实战建议
说了这么多,最后给几条实在的建议:
- 优先用互斥锁——简单可靠,不容易出错。等性能瓶颈出现时再考虑优化。
- 结构体设计时就把锁考虑进去——别等出问题了再加锁,那时候改代码成本高。
- 写单元测试——多线程的bug很难复现,写几个线程同时读写,跑个几千次看看会不会崩。
- 用工具检查——Valgrind的helgrind工具能检测数据竞争,我每次上线前都会跑一遍。
嗯,结构体与多线程这块,说白了就是“数据安全”四个字。你只要记住:共享数据要加锁,生命周期要管好,指针别乱飞。做到这三点,大部分问题都能避免。