基于Pthreads的程序在macOS崩溃但Linux正常的原因咨询
多线程程序在macOS崩溃但Linux正常的原因分析
问题背景
我有一个基于Pthreads的多线程程序,包含生产者、消费者、打印三个线程:
- 生产者线程往链表中添加元素,直到链表元素数达到10
- 消费者线程从链表头部删除元素,直到链表为空
- 打印线程持续遍历并打印链表内容
程序在macOS(Darwin Kernel Version 19.6.0)运行几秒后就会崩溃,但在Linux系统中却能正常运行。如果给printer线程的print_list调用前后加上pthread_mutex_lock/unlock,macOS下程序就能正常运行;但Linux下即使不加锁,程序也不会崩溃。我想知道这是Linux线程调度策略不同导致的,还是存在其他原因?
完整程序代码
prod_consume.c
#include<stdio.h> #include<string.h> #include<pthread.h> #include<stdlib.h> #include<unistd.h> #include "list.h" #define RANGE_MIN 1 #define RANGE_MAX 10 pthread_t tid[3]; list_t list; pthread_mutex_t queue_lock; /* Return a uniformly random number in the range [low,high]. */ int random_range (unsigned const low, unsigned const high) { unsigned const range = high - low + 1; return low + (int) (((double) range) * rand() / (RAND_MAX + 1.0)); } /* * consumer thread function */ void* consumer(void *arg) { static int val = 0; while (1) { //sleep(1); pthread_mutex_lock(&queue_lock); while (list.total_elem > 0) consume_and_delete(&list); pthread_mutex_unlock(&queue_lock); } } void *producer(void *arg) { while (1) { //sleep(1); pthread_mutex_lock(&queue_lock); while (list.total_elem < 10) add_to_list(&list, random_range(RANGE_MIN, RANGE_MAX)); pthread_mutex_unlock(&queue_lock); } } /* * printer thread function */ void* printer(void *arg) { while (1) { //pthread_mutex_lock(&queue_lock); //lines deliberately commented out to show the crash print_list(&list); //pthread_mutex_unlock(&queue_lock); } } int main(void) { int ret; if (pthread_mutex_init(&queue_lock, NULL) != 0) { printf("\n mutex init failed\n"); return 1; } ret = pthread_create(&(tid[1]), NULL, &consumer, NULL); if (ret != 0) { printf("\ncan't create thread :[%s]", strerror(ret)); } ret = pthread_create(&(tid[0]), NULL, &producer, NULL); if (ret != 0) { printf("\ncan't create thread :[%s]", strerror(ret)); } ret = pthread_create(&(tid[2]), NULL, &printer, NULL); if (ret != 0) { printf("\ncan't create thread :[%s]", strerror(ret)); } pthread_join(tid[0], NULL); pthread_join(tid[1], NULL); pthread_join(tid[2], NULL); pthread_mutex_destroy(&queue_lock); return 0; }
list.h
#ifndef __LIST_H__ #define __LIST_H__ typedef struct node_ { int num; struct node_ *next; } node_t; typedef struct list_ { int total_elem; node_t *head; } list_t; #define TRUE 1 #define FALSE 0 void add_to_list(list_t *list, int num); node_t *allocate_new(int num); void consume_and_delete(list_t *list); void print_list(list_t *list); #endif
list.c
#include <stdio.h> #include <stdlib.h> #include "list.h" void add_to_list(list_t *list, int val) { node_t *tmp; node_t *new; new = allocate_new(val); if (!new) { printf("%s: allocation failure \n", __FUNCTION__); } printf("%s: Enqueueing %d\n", __FUNCTION__, new->num); if (!list) { return; } if (!list->head) { list->total_elem++; list->head = new; return; } tmp = list->head; while (tmp->next) { tmp = tmp->next; } tmp->next = new; list->total_elem++; } node_t *allocate_new(int num) { node_t *tmp; tmp = malloc(sizeof(node_t)); if (!tmp) { printf("%s: failed in malloc\n", __FUNCTION__); return NULL; } tmp->num = num; tmp->next = NULL; return tmp; } /* reads from the front of the queue and deletes the element */ void consume_and_delete(list_t *list) { node_t *tmp, *next; if (!list->head) { return; } tmp = list->head; next = tmp->next; printf("%s: Dequeueing %d\n", __FUNCTION__,tmp->num); list->head = next; list->total_elem--; free(tmp); } void print_list(list_t *list) { node_t *tmp; if (!list->head) { printf("%s: queue empty \n", __FUNCTION__); } tmp = list->head; while (tmp) { printf("%d ", tmp->num); tmp = tmp->next; } printf("\n"); }
Makefile
all: prodcon prodcon: list.o prod_consume.o gcc -g -o prodcon list.o prod_consume.o -lpthread list.o: list.c list.h gcc -g -c -o list.o list.c prod_consume.o: prod_consume.c list.h gcc -g -c -o prod_consume.o prod_consume.c clean: rm -f *.o ./prodcon
原因分析
核心本质:未定义行为的随机性
程序的问题根源是数据竞争:生产者和消费者通过锁同步了链表操作,但打印线程完全没有同步,直接访问共享的链表结构。这种场景下,程序的行为属于C标准定义的未定义行为——未定义行为的结果是完全不可预测的,可能崩溃、可能输出错误结果、也可能“看起来正常”,不同系统或不同运行次的表现都可能不同。Linux下没崩溃只是运气好,不是程序本身正确。
导致跨平台差异的具体因素
线程调度策略差异
- macOS的线程调度可能更频繁地切换线程,导致打印线程更大概率在链表修改的中间状态(比如生产者还没完成尾节点的
next指针赋值、消费者刚free了节点但打印线程还在访问该节点)时执行,触发空指针访问或野指针访问,直接导致崩溃。 - Linux默认采用分时调度策略,可能让生产者/消费者线程持有锁的时间更连续,打印线程插入执行的时机刚好避开了链表的不一致状态,从而没触发崩溃。
- macOS的线程调度可能更频繁地切换线程,导致打印线程更大概率在链表修改的中间状态(比如生产者还没完成尾节点的
编译器与内存模型差异
- macOS默认用Clang编译,Linux默认用GCC,两者的优化策略、内存访问重排规则不同。比如在macOS下,打印线程可能看到未同步的
total_elem和链表指针的矛盾值(比如total_elem显示有元素,但head已经被消费者free);而Linux下可能因为缓存一致性或编译器隐含的内存屏障,刚好没出现这种矛盾。
- macOS默认用Clang编译,Linux默认用GCC,两者的优化策略、内存访问重排规则不同。比如在macOS下,打印线程可能看到未同步的
链表操作的非原子性
- 链表的添加、删除操作都是多步非原子操作:
add_to_list需要遍历到尾节点、设置next指针、更新total_elem,三步操作之间线程可能切换;consume_and_delete需要修改head、更新total_elem、free节点,同样是多步操作。
打印线程在这些步骤中间访问链表,就可能拿到“半修改”的无效链表结构,进而触发崩溃。
- 链表的添加、删除操作都是多步非原子操作:
为什么加锁后都正常
给打印线程加上锁后,所有访问共享链表的线程都通过同一把互斥锁同步,确保打印线程只有在链表处于一致状态时才能访问,彻底消除了数据竞争,所以两个系统都能稳定运行。
结论
Linux下程序没崩溃只是未定义行为的一种表现,不是Linux的线程调度“更安全”,也不是程序本身没有问题。正确的做法是给所有访问共享链表的线程都加上互斥锁同步,才能保证跨平台的正确性和稳定性。
内容的提问来源于stack exchange,提问作者HarperDada
相关产品推荐
相关产品推荐

