pthread_cond_destroy死锁卡住问题处理记录

问题

供应商代码, 在退出某线程时, 销毁条件变量的过程中, 线程被阻塞.

在这里插入图片描述

参考手册

参看man手册, 销毁其它线程正在等待的cond将导致不确定行为:

pthread_cond_destroy()
It  shall be safe to destroy an initialized condition variable upon which no threads are currently blocked. Attempting to destroy a condition variable upon which other threads are currently blocked results in undefined behavior.

因此在销毁之前, 先发送pthread_cond_broadcast(&pEvent->cond);
通知所有等待线程:

int32_t osEventDestroy(osEvent *pEvent)
{
    pthread_mutex_lock(&pEvent->mutex);
    pthread_cond_broadcast(&pEvent->cond);
    pthread_mutex_unlock(&pEvent->mutex);

    pthread_cond_destroy(&pEvent->cond);
    pthread_mutex_destroy(&pEvent->mutex); 

再次测试, 问题还是有概率出现.

查看源码

查看源码描述, __pthread_cond_destroy 默认有其它线程在等待, 因此将会等待__wrefs变量的值:


/* See __pthread_cond_wait for a high-level description of the algorithm.

   A correct program must make sure that no waiters are blocked on the condvar
   when it is destroyed, and that there are no concurrent signals or
   broadcasts.  To wake waiters reliably, the program must signal or
   broadcast while holding the mutex or after having held the mutex.  It must
   also ensure that no signal or broadcast are still pending to unblock
   waiters; IOW, because waiters can wake up spuriously, the program must
   effectively ensure that destruction happens after the execution of those
   signal or broadcast calls.
   Thus, we can assume that all waiters that are still accessing the condvar
   have been woken.  We wait until they have confirmed to have woken up by
   decrementing __wrefs.  */
int
__pthread_cond_destroy (pthread_cond_t *cond)
{
  LIBC_PROBE (cond_destroy, 1, cond);

  /* Set the wake request flag.  We could also spin, but destruction that is
     concurrent with still-active waiters is probably neither common nor
     performance critical.  Acquire MO to synchronize with waiters confirming
     that they finished.  */
  unsigned int wrefs = atomic_fetch_or_acquire (&cond->__data.__wrefs, 4);
  int private = __condvar_get_private (wrefs);
  while (wrefs >> 3 != 0)
    {
      futex_wait_simple (&cond->__data.__wrefs, wrefs, private);
      /* See above.  */
      wrefs = atomic_load_acquire (&cond->__data.__wrefs);
    }
  /* The memory the condvar occupies can now be reused.  */
  return 0;
}

打印销毁之前__wrefs的为-8, 不可理喻. 尝试将其强制清零之后, 问题消失.

    //
int32_t osEventDestroy(osEvent *pEvent)
{
    pthread_mutex_lock(&pEvent->mutex);
    pthread_cond_broadcast(&pEvent->cond);
    pthread_mutex_unlock(&pEvent->mutex);
	if(0 != pEvent->cond.__data.__wrefs)
	{
		OSLAYER_ERR("%p %s cond error with refs %d\n",pEvent,__func__,pEvent->cond.__data.__wrefs);
		pEvent->cond.__data.__wrefs = 0;
	}
    pthread_cond_destroy(&pEvent->cond);
    pthread_mutex_destroy(&pEvent->mutex); 

追查原因

查阅代码, 没有其它更多的线程在使用该变量, 那么为啥该值会异常呢?
最后发现, 是因为源码在使用条件变量时, 先启动了等待线程pthread_cond_wait, 再进行了cond的初始化.
也就是说,pthread_cond_wait带入了条件变量的时候, 该条件变量并没有初始化, 执行完成了pthread_cond_wait之后, 才调用了pthread_cond_init初始化变量.

调整代码逻辑之后, 问题消失.

结论:

使用未初始化的条件变量, 函数不会报错,但可能产生执行异常.

``` Task stTaskQueue[256]; int iTaskCount = 0; pthread_mutex_t stMutexQueue; pthread_cond_t stCondQueue; void* PthreadPoolWorker(void* arg) { Task* pstTask; while(g_iExit == 0) { pthread_mutex_lock(&stMutexQueue); while(iTaskCount == 0) { pthread_cond_wait(&stCondQueue, &stMutexQueue); } pstTask = &stTaskQueue[--iTaskCount]; pthread_mutex_unlock(&stMutexQueue); pstTask->task(pstTask->arg); } } int ThreadPoolSubmit(void* (*task)(void*),void* arg) { int iRet = -1; if(pthread_mutex_lock(&stMutexQueue) != 0) { cmn_PrtError("Error in locking mutex"); } if(iTaskCount > 255) { pthread_mutex_unlock(&stMutexQueue); cmn_PrtError("Error in queue overflow"); } stTaskQueue[iTaskCount].task = task; stTaskQueue[iTaskCount].arg = arg; iTaskCount++; if(pthread_mutex_unlock(&stMutexQueue) != 0) { cmn_PrtError("Error in unlocking mutex"); } if(pthread_cond_signal(&stCondQueue) < 0) { cmn_PrtError("Error in signaling condition"); } iRet = 0; _Exit: return iRet; } int ThreadPoolInit(void) { int iRet = -1; if(pthread_mutex_init(&stMutexQueue, NULL) <0) { cmn_PrtError("Error in initializing mutex"); } if(pthread_cond_init(&stCondQueue, NULL)) { cmn_PrtError("Error in initializing condition"); } for(int i = 0; i < 5; i++) { pthread_t stThread; pthread_create(&stThread, NULL, PthreadPoolWorker, NULL); pthread_detach(stThread); } iRet = 0; _Exit: return iRet; }```int ThreadPoolDestroy(void) { int iRet = -1; g_iExit = 1; if(pthread_mutex_lock(&stMutexQueue)!= 0) { cmn_PrtError("Error in locking mutex"); } while(iTaskCount > 0) { pthread_cond_wait(&stCondQueue, &stMutexQueue); } if(pthread_mutex_unlock(&stMutexQueue)!= 0) { cmn_PrtError("Error in unlocking mutex"); } if(pthread_cond_broadcast(&stCondQueue) < 0) { cmn_PrtError("Error in broadcasting condition"); } if(pthread_mutex_destroy(&stMutexQueue)!= 0) { cmn_PrtError("Error in destroying mutex"); } if(pthread_cond_destroy(&stCondQueue)!= 0) { cmn_PrtError("Error in destroying condition"); } iRet = 0; _Exit: return iRet; } 检查代码是否有bug,并给出解决方案
最新发布
03-13
<think>嗯,我现在需要仔细检查用户提供的线程池代码,看看有没有潜在的bug。首先,我应该先通读整个代码,了解各个函数的作用和流程。代码包括初始化函数ThreadPoolInit,提交任务的ThreadPoolSubmit,工作线程函数PthreadPoolWorker,以及销毁函数ThreadPoolDestroy。另外,还有一些全局变量,比如任务队列stTaskQueue,互斥锁stMutexQueue,条件变量stCondQueue,任务计数iTaskCount和退出标志g_iExit。 首先,从ThreadPoolInit开始看。初始化互斥锁和条件变量,然后创建5个工作线程,并将它们分离。这里可能有问题的是,pthread_create的返回值是否正确处理了错误。比如,pthread_create如果失败的话,这里并没有处理,而是继续循环,可能导致线程数量不足。此外,创建的线程是否应该被detach,这取决于设计,但一般来说,如果线程被分离了,主线程就无法join它们,可能在销毁时无法正确等待所有任务完成。 接下来是ThreadPoolSubmit函数。这里有一个问题,当任务队列已满(iTaskCount >255)时,解锁互斥锁后直接打印错误并返回。但用户提交任务时可能希望等待队列有空位,而这里直接返回错误,可能导致任务被丢弃。不过根据代码设计,可能这是预期的行为,即队列满时拒绝任务。不过代码中没有处理这种情况的返回值,iRet在错误情况下可能没有被正确设置。例如,当iTaskCount >255时,解锁后执行cmn_PrtError,但之后iRet还是-1吗?代码中有iRet =0在后面,但如果在错误情况下是否会执行到这里?例如,当iTaskCount超过255时,函数在cmn_PrtError之后可能直接goto _Exit,此时iRet还是-1吗? 再看ThreadPoolSubmit中的错误处理。例如,在pthread_mutex_lock失败后,cmn_PrtError被调用,但之后是否返回?代码中使用了goto _Exit,但是在最初的if语句中没有看到goto,只有在后面的错误处理中有。例如,当pthread_mutex_lock失败时,打印错误,但之后可能继续执行后面的代码,比如检查iTaskCount是否超过255。这样可能导致未定义行为,因为互斥锁可能没有被成功锁定,但后面的代码还在操作共享资源。这显然是一个问题。正确的做法是在锁失败后,直接返回错误,而不是继续执行。 同样,在pthread_mutex_unlock失败时,是否处理了错误?例如,在提交任务时,如果解锁失败,可能仍然持有锁,导致死锁或其他问题。但代码中在解锁失败后打印错误,但没有返回,而是继续执行后面的代码,比如发送条件变量信号。这可能导致后续操作在未释放锁的情况下进行,从而引发问题。 然后是工作线程函数PthreadPoolWorker。这里有一个while循环,检查g_iExit是否为0。当g_iExit被设置为1时,线程会退出循环。但在销毁函数中,当调用g_iExit=1后,工作线程可能还在等待条件变量。此时,销毁函数会等待iTaskCount为0,然后广播条件变量,唤醒所有等待的线程,让它们检查g_iExit是否为0。但这里可能存在竞争条件,例如,当销毁函数设置g_iExit=1之后,但工作线程可能被唤醒后,发现iTaskCount不为0,继续处理任务,直到队列为空。此时,销毁函数中的循环等待iTaskCount为0,而工作线程在处理完任务后再次进入等待条件变量的状态。此时,销毁函数可能无法唤醒它们,因为条件变量可能已经被触发,导致销毁函数中的pthread_cond_wait可能无法被唤醒,从而导致死锁。 另一个问题是在销毁函数ThreadPoolDestroy中,设置g_iExit=1之后,是否应该先唤醒所有工作线程?因为在设置g_iExit=1之后,工作线程可能在等待条件变量,而销毁函数在锁住互斥锁后,等待iTaskCount为0。这时候,如果任务队列已经为空,工作线程可能在等待条件变量,而销毁函数在等待iTaskCount为0,此时iTaskCount已经是0,所以循环不会执行,直接解锁,然后广播条件变量。此时工作线程被唤醒,检查g_iExit是否为0,此时已经是1,所以退出循环,线程结束。这可能没问题。但如果任务队列中还有任务,销毁函数会等待iTaskCount变为0,然后解锁,并广播。此时工作线程被唤醒,处理任务,直到队列为空,然后再次进入等待。此时,销毁函数已经广播了条件变量,但工作线程可能再次进入等待,导致无法退出。因此,可能需要在线程函数中,在检查g_iExit时,即使被唤醒,如果g_iExit是1,就退出循环,不管队列是否为空。目前的代码中,工作线程的循环条件是while(g_iExit ==0),所以当g_iExit变为1时,线程会退出循环,结束运行。但是在销毁函数中,设置g_iExit=1之后,是否需要唤醒所有线程,确保它们能够检查到g_iExit的变化? 另外,销毁函数中的逻辑是否正确?当销毁函数调用pthread_cond_broadcast时,是否在正确的锁状态下?销毁函数在设置g_iExit=1之后,先锁定互斥锁,然后等待iTaskCount为0。这时候,如果其他线程还在处理任务,iTaskCount可能还未减到0。当iTaskCount减到0时,销毁函数会解锁,然后广播条件变量。但此时,工作线程可能已经被唤醒,处理任务,但g_iExit已经设置为1,所以它们会在处理完任务后退出循环。这可能没问题,但需要确认。 接下来,检查互斥锁和条件变量的使用是否正确。例如,在PthreadPoolWorker中,当调用pthread_cond_wait时,会释放互斥锁,并在被唤醒时重新获得锁。这里,当iTaskCount==0时,线程会等待条件变量。当任务被提交时,条件变量被signal,唤醒一个线程。但如果有多个工作线程,可能应该使用broadcast吗?不过这里使用signal可能没问题,因为每次提交一个任务,唤醒一个线程。但如果任务队列中有多个任务,可能需要唤醒多个线程。例如,当一次提交多个任务时,可能需要broadcast,但代码中使用的是signal,每次只唤醒一个线程。这可能导致任务处理不够及时,但不会引发错误,只是效率问题。 另一个问题是任务队列的实现是否是线程安全的。例如,当多个工作线程同时被唤醒,它们会竞争互斥锁,然后其中一个线程取出任务。这里iTaskCount被减1,是正确的。但是任务队列是数组结构,当取出任务时,是取最后一个元素(因为iTaskCount先减1,然后取stTaskQueue[--iTaskCount])。这相当于将任务队列当作栈使用,后进先出。这可能影响任务的执行顺序,但不会导致错误,除非有特定顺序要求。 在ThreadPoolSubmit中,当添加任务到队列时,是直接写入stTaskQueue[iTaskCount],然后iTaskCount++。这应该是正确的,因为互斥锁已经锁定,所以不会有竞争。 另外,全局变量g_iExit的访问是否被互斥锁保护?在销毁函数中,设置g_iExit=1时,并没有在锁的保护下进行。同样,工作线程在检查g_iExit时也没有在锁的保护下。这可能导致竞争条件,例如,当销毁函数设置g_iExit=1时,工作线程可能看不到这个改变,因为内存可见性问题。正确的做法应该是将g_iExit的访问也放在互斥锁的保护下,或者使用原子操作。例如,在销毁函数中,设置g_iExit=1时,是否需要先锁定互斥锁?或者在访问g_iExit时,使用原子变量或者内存屏障。否则,工作线程可能一直循环,因为g_iExit的值没有被更新到工作线程的缓存中。 还有一个问题是在销毁函数中,当调用pthread_cond_wait时,是否在循环中?例如,在销毁函数中: while(iTaskCount > 0) { pthread_cond_wait(&stCondQueue, &stMutexQueue); } 但是,当pthread_cond_wait被唤醒时,iTaskCount可能已经被其他线程修改,所以必须使用循环来重新检查条件。这里是正确的,因为代码中使用了while循环。 另外,销毁函数在等待iTaskCount为0后,解锁互斥锁,然后广播条件变量。之后销毁互斥锁和条件变量。这可能有问题,因为此时可能还有工作线程在运行,或者被唤醒后尝试获取已经销毁的互斥锁或条件变量。例如,工作线程可能在销毁函数调用pthread_cond_broadcast之后,被唤醒,然后尝试获取互斥锁,但此时互斥锁已经被销毁,导致未定义行为。 所以,正确的销毁流程应该是: 1. 设置退出标志g_iExit=1。 2. 广播所有工作线程,让它们退出。 3. 等待所有工作线程结束(通过join)。 4. 销毁互斥锁和条件变量。 但在这段代码中,工作线程是被detach的,无法被join。因此,销毁函数可能无法确保所有工作线程已经退出,导致在销毁互斥锁和条件变量时,仍有工作线程在使用它们,从而引发错误。 总结可能的bug: 1. **g_iExit的访问没有互斥保护**:工作线程检查g_iExit时,可能读取到过期的值,因为该变量没有被原子访问或保护在互斥锁内。 2. **销毁函数中互斥锁和条件变量的销毁时机问题**:可能在销毁时仍有工作线程在使用这些资源,导致未定义行为。 3. **线程创建失败未处理**:ThreadPoolInit中pthread_create的返回值未检查,可能导致线程数量不足。 4. **ThreadPoolSubmit中的错误处理不完善**:例如,在pthread_mutex_lock失败后,没有立即返回,导致后续代码在未获得锁的情况下操作共享数据。 5. **任务队列满时的处理**:直接返回错误,但可能应该处理或等待。 6. **条件变量的使用**:在ThreadPoolSubmit中使用pthread_cond_signal可能不够,当有多个任务时,应使用pthread_cond_broadcast以提高效率,但这不是错误。 7. **销毁函数中可能死锁**:当工作线程在收到退出信号后,可能无法及时退出,导致销毁函数无法继续。 8. **detach线程可能导致资源访问后销毁**:工作线程被detach,主线程无法等待它们结束,销毁时可能还在运行。 解决方案建议: 1. **对g_iExit的访问加锁**:在设置和读取g_iExit时使用互斥锁,确保可见性。 2. **在销毁函数中,确保所有工作线程已退出**:不要detach线程,而是join它们,但需要记录线程ID。 3. **处理线程创建失败的情况**:在ThreadPoolInit中检查pthread_create的返回值。 4. **修复ThreadPoolSubmit中的错误处理流程**:在加锁失败后,直接返回错误,不继续执行。 5. **在销毁函数中,正确销毁资源**:确保所有线程已退出,再销毁互斥锁和条件变量。 6. **使用原子操作或volatile修饰g_iExit**:确保线程间可见性。 具体修改步骤: - 将g_iExit声明为volatile,或者在访问时加锁。 - 在ThreadPoolInit中,保存创建的线程ID,以便后续join。 - 在ThreadPoolDestroy中,join所有工作线程,确保它们已退出后再销毁资源。 - 在ThreadPoolSubmit中,修正错误处理流程,确保加锁失败后立即返回。 - 在销毁函数中,先设置g_iExit=1,然后广播所有线程,唤醒它们,然后等待它们退出。 例如,修改后的ThreadPoolDestroy可能如下: int ThreadPoolDestroy(void) { int iRet = -1; g_iExit = 1; // 广播所有工作线程,唤醒它们以检查g_iExit pthread_cond_broadcast(&stCondQueue); // 等待所有工作线程退出(假设保存了线程ID数组) for (int i = 0; i < 5; i++) { pthread_join(threads[i], NULL); } // 销毁互斥锁和条件变量 pthread_mutex_destroy(&stMutexQueue); pthread_cond_destroy(&stCondQueue); iRet = 0; _Exit: return iRet; } 但原代码中没有保存线程ID,因此无法join。因此,需要在ThreadPoolInit中保存创建的线程ID到一个数组中,这样才能在销毁时join。这需要修改全局变量,添加一个pthread_t数组。 此外,工作线程函数中,当g_iExit设置为1时,应退出循环。但需要确保在唤醒所有线程后,它们能够及时检查到g_iExit的变化。 另一个问题是,在ThreadPoolDestroy中,设置g_iExit=1后,应该先锁定互斥锁,然后广播,再解锁,以确保线程在检查g_iExit时能够正确同步。例如: pthread_mutex_lock(&stMutexQueue); g_iExit = 1; pthread_cond_broadcast(&stCondQueue); pthread_mutex_unlock(&stMutexQueue); 这样,工作线程在检查g_iExit时,需要先获得锁,或者将g_iExit的访问放在锁的保护下。 例如,修改工作线程循环: while(1) { pthread_mutex_lock(&stMutexQueue); if (g_iExit) { pthread_mutex_unlock(&stMutexQueue); break; } // 检查任务队列... } 但这样可能会影响性能。或者,在每次循环时检查g_iExit,但需要确保可见性。 综上,主要问题在于线程退出机制和资源销毁的同步,以及错误处理的完整性。</think>以下是对代码的分析及解决方案: --- ### **代码问题分析** #### 1. **全局退出标志未同步(严重)** - `g_iExit` 作为全局退出标志,其读写**缺乏互斥锁保护**,存在内存可见性问题。工作线程可能无法及时感知到退出标志的变化。 - **修复**:将对`g_iExit`的访问包裹在互斥锁中,或使用原子操作(如C11原子类型)。 #### 2. **线程销毁逻辑缺陷(严重)** - 销毁函数`ThreadPoolDestroy`中: - 未确保工作线程已完全退出,直接销毁互斥锁和条件变量,可能导致**线程访问已销毁资源**(崩溃风险)。 - 使用`pthread_detach`后无法`join`线程,无法安全等待线程退出。 - **修复**: - 保存线程ID以便`pthread_join`。 - 在销毁前通过条件变量广播唤醒所有线程,并等待其退出。 #### 3. **任务提交错误处理不完整** - `ThreadPoolSubmit`中: - 若`pthread_mutex_lock`失败,未立即返回,继续操作共享资源(如`iTaskCount`),导致**数据竞争**。 - **修复**:所有锁失败后直接跳转到错误处理,避免后续操作。 #### 4. **任务队列溢出处理不当** - 当`iTaskCount > 255`时,直接返回错误,但未处理队列已满时的阻塞提交需求。 - **修复**(可选):根据设计需求,可改为阻塞等待队列空闲,或维持现有策略但明确错误码。 #### 5. **线程创建未检查返回值** - `pthread_create`可能失败(如系统资源不足),但未处理返回值,导致**线程数量不足**。 - **修复**:检查返回值并记录错误。 --- ### **解决方案** #### 1. **同步退出标志** ```c // 修改全局变量声明 volatile int g_iExit = 0; // 使用volatile确保可见性(需配合互斥锁) // 工作线程循环修改为: pthread_mutex_lock(&stMutexQueue); while (iTaskCount == 0 && g_iExit == 0) { // 检查退出标志 pthread_cond_wait(&stCondQueue, &stMutexQueue); } if (g_iExit != 0) { pthread_mutex_unlock(&stMutexQueue); break; // 退出线程 } // ... 取任务逻辑 ``` #### 2. **修复线程销毁逻辑** ```c // 全局保存线程ID pthread_t g_astThreads[5]; // 假设线程池固定5线程 // ThreadPoolInit中保存线程ID for(int i = 0; i < 5; i++) { if (pthread_create(&g_astThreads[i], NULL, PthreadPoolWorker, NULL) != 0) { cmn_PrtError("Failed to create thread"); goto _Exit; } // 移除pthread_detach } // ThreadPoolDestroy中等待线程退出 g_iExit = 1; pthread_mutex_lock(&stMutexQueue); pthread_cond_broadcast(&stCondQueue); // 唤醒所有线程 pthread_mutex_unlock(&stMutexQueue); for (int i = 0; i < 5; i++) { pthread_join(g_astThreads[i], NULL); } ``` #### 3. **完善任务提交错误处理** ```c int ThreadPoolSubmit(...) { if (pthread_mutex_lock(&stMutexQueue) != 0) { cmn_PrtError("Lock failed"); goto _Exit; // 直接跳转,不操作共享数据 } // ... 后续逻辑 } ``` #### 4. **处理线程创建失败** ```c int ThreadPoolInit() { for(int i = 0; i < 5; i++) { if (pthread_create(...) != 0) { cmn_PrtError("Thread create failed"); // 清理已创建的线程 for (int j = 0; j < i; j++) pthread_cancel(g_astThreads[j]); iRet = -1; goto _Exit; } } } ``` #### 5. **其他优化** - 将任务队列改为环形缓冲区,支持先进先出。 - 条件变量使用`pthread_cond_broadcast`替代`pthread_cond_signal`,提高多任务并发效率。 --- ### **总结** 修复后的代码通过**同步退出标志**、**完善线程生命周期管理**和**严格错误处理**,可避免竞态条件和资源泄漏,确保线程池安全退出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值