Mac/Xcode下生产者消费者队列缓冲区多线程异常排查
问题现象
- 编写生产者-消费者多线程程序时,使用栈作为缓冲区运行完全正常;替换为链式队列实现后,程序总会在
printQueue()函数访问currentSlot->num时触发内存访问异常 - 异常时
bufferSize计数和队列实际长度不匹配,会出现计数比实际长度大1的情况,遍历队列时会访问到已释放的野指针,打印出随机垃圾值 - 尝试更换信号量库、复制网上的修复版代码运行,问题仍复现,怀疑是Mac设备+Xcode环境导致的异常
根因分析
问题是三个代码bug叠加MacOS命名信号量的特性导致的,和Xcode开发环境本身无关:
- 未初始化互斥锁:代码中定义的
static pthread_mutex_t bufferMutex没有做初始化,直接加锁解锁属于未定义行为,互斥锁可能根本没起到互斥作用,多线程同时修改队列结构直接导致内存错乱 - 队列边界逻辑缺失:消费队列最后一个节点时,只更新了
startSlot指针,没有将endSlot置为NULL,导致endSlot成为指向已释放内存的野指针。下次生产时如果bufferSize计数错误,没有触发头尾指针重置逻辑,就会向已释放的内存地址写数据,破坏内存结构 - 命名信号量残留导致同步失效:MacOS下
sem_open创建的命名信号量是系统级持久化资源,程序异常退出(比如调试时触发崩溃)时不会自动清理残留信号量。下次运行程序调用sem_open打开同名信号量时,不会重置初始计数值,会直接复用残留的错误计数,直接导致生产者消费者的同步逻辑完全失效,多线程并发操作缓冲区,不管后续怎么改代码,只要信号量名不变,问题就会一直复现。栈实现时不需要维护头尾指针,内存结构简单,所以没有触发明显崩溃。
修复方案
按以下步骤修改代码即可解决问题:
- 初始化互斥锁
定义互斥锁时直接用宏初始化,或者在main函数开头手动初始化:// 方式1:定义时直接初始化 static pthread_mutex_t bufferMutex = PTHREAD_MUTEX_INITIALIZER; // 方式2:main函数开头调用初始化函数 // pthread_mutex_init(&bufferMutex, NULL); - 修复消费逻辑的边界bug
在consume()函数中释放队头节点后,增加队列空判断,重置尾指针:static int consume(void) { int ret; if (shouldConsume()) { slot_t *deletedSlot = startSlot; startSlot = deletedSlot->next; int deletedNum = deletedSlot->num; free(deletedSlot); deletedSlot = NULL; // 新增:队列空时重置尾指针,避免野指针 if (startSlot == NULL) { endSlot = NULL; } bufferSize--; ret = deletedNum; } else { printf("%s: ERROR: Tried to consume from an empty buffer!!!\n", loggingPrefix); ret = -1; } printQueue(); return ret; } - 清理残留信号量,避免计数错误
在main函数中调用sem_open之前,先主动unlink对应名称的信号量,清除系统中残留的旧资源:// 先清理残留的同名信号量 sem_unlink(EMPTY_SEMAPHORE_ID); sem_unlink(FULL_SEMAPHORE_ID); // 再创建新的信号量 if((emptySemaphore = sem_open(EMPTY_SEMAPHORE_ID, O_CREAT, 0644, MAX_BUFFER_SLOTS)) == SEM_FAILED || (fullSemaphore = sem_open(FULL_SEMAPHORE_ID, O_CREAT, 0644, 0)) == SEM_FAILED) { perror("sem_open"); exit(EXIT_FAILURE); }
修改完成后重新运行程序,队列遍历异常、计数不匹配的问题就会完全消失。
内容的提问来源于stack exchange,提问作者LaChoca
相关产品推荐
相关产品推荐

