一个 pthread_cancel 引起的线程死锁

本文通过一个实例程序探讨了在Linux环境下pthread_cancel函数导致的线程死锁问题,详细解释了取消点和取消类型的实现,并提供了一种避免死锁的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  这篇文章主要从一个 Linux 下一个 pthread_cancel 函数引起的多线程死锁小例子出发来说明 Linux 系统对 POSIX 线程取消点的实现方式,以及如何避免因此产生的线程死锁。

1.一个 pthread_cancel 引起的线程死锁小例子

  下面是一段在Linux 平台下能引起线程死锁的小例子。这个实例程序仅仅是使用了条件变量和互斥量进行一个简单的线程同步,thread0 首先启动,锁住互斥量 mutex,然后调用 pthread_cond_wait,它将线程 tid[0] 放在等待条件的线程列表上后,对 mutex 解锁。thread1 启动后等待 10 秒钟,此时 pthread_cond_wait 应该已经将 mutex 解锁,这时 tid[1] 线程锁住 mutex,然后广播信号唤醒 cond 等待条件的所有等待线程,之后解锁 mutex。当 mutex 解锁后,tid[0] 线程的pthread_cond_wait 函数重新锁住 mutex 并返回,最后 tid[0] 再对 mutex 进行解锁。

#include <pthread.h>
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void* thread0(void* arg)
{
    pthread_mutex_lock(&mutex);
    printf("in thread 0 tag 1\n");
    pthread_cond_wait(&cond, &mutex);
    printf("in thread 0 tag 2\n");
    pthread_mutex_unlock(&mutex);
    printf("in thread 0 tag 3\n");
    pthread_exit(NULL);
}

void* thread1(void* arg)
{
    sleep(10);
    printf("in thread 1 tag 1\n");
    pthread_mutex_lock(&mutex);
    printf("in thread 1 tag 2\n");
    pthread_cond_broadcast(&cond);
    pthread_mutex_unlock(&mutex);
    printf("in thread 1 tag 3\n");
    pthread_exit(NULL);
}
int main()
{
    pthread_t tid[2];
    if (pthread_create(&tid[0], NULL, thread0, NULL) != 0) 
    {
        exit(1);
    }
    if (pthread_create(&tid[1], NULL, thread1, NULL) != 0) 
    {
        exit(1);
    }
    sleep(5);
    printf("in main thread tag 1\n");
    pthread_cancel(tid[0]);

    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

示例代码_对上述程序的跟踪,注意执行的顺序。

(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000400aa8 in main at cancel.c:34
2       breakpoint     keep y   0x0000000000400ad1 in main at cancel.c:38
3       breakpoint     keep y   0x0000000000400a0d in thread0 at cancel.c:13
4       breakpoint     keep y   0x0000000000400b12 in main at cancel.c:44
5       breakpoint     keep y   0x0000000000400b1e in main at cancel.c:46
6       breakpoint     keep y   0x0000000000400a64 in thread1 at cancel.c:24
7       breakpoint     keep y   0x0000000000400b2f in main at cancel.c:47
(gdb) r
Starting program: /home/xsc/work/misec/cancel 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main () at cancel.c:34
34      if (pthread_create(&tid[0], NULL, thread0, NULL) != 0) 
(gdb) 
(gdb) bt
#0  main () at cancel.c:34
(gdb) n
[New Thread 0x7ffff77f6700 (LWP 3394)]
in thread 0 tag 1
[Switching to Thread 0x7ffff77f6700 (LWP 3394)]

Breakpoint 3, thread0 (arg=0x0) at cancel.c:13
13      pthread_cond_wait(&cond, &mutex);
(gdb) n
[Switching to Thread 0x7ffff7fdd740 (LWP 3390)]

Breakpoint 2, main () at cancel.c:38
38      if (pthread_create(&tid[1], NULL, thread1, NULL) != 0) 
(gdb) n
[New Thread 0x7ffff6ff5700 (LWP 3395)]
42      sleep(5);
(gdb) n
in thread 1 tag 1
[Switching to Thread 0x7ffff6ff5700 (LWP 3395)]

Breakpoint 6, thread1 (arg=0x0) at cancel.c:24
24      pthread_mutex_lock(&mutex);
(gdb) n
25      printf("in thread 1 tag 2\n");
(gdb) n
in main thread tag 1
in thread 1 tag 2
26      pthread_cond_broadcast(&cond);
(gdb) n
[Switching to Thread 0x7ffff7fdd740 (LWP 3390)]

Breakpoint 4, main () at cancel.c:44
44      pthread_cancel(tid[0]);
(gdb) n
in thread 1 tag 3
in thread 0 tag 2
in thread 0 tag 3

Breakpoint 5, main () at cancel.c:46
46      pthread_join(tid[0], NULL);
(gdb) n
[Thread 0x7ffff6ff5700 (LWP 3395) exited]
[Thread 0x7ffff77f6700 (LWP 3394) exited]

Breakpoint 7, main () at cancel.c:47

我们发现:

Breakpoint 7, main () at cancel.c:47
^C

   一直卡在这里。看起来似乎没有什么问题,但是 main 函数调用了一个pthread_cancel 来取消 tid[0] 线程。上面程序编译后运行时会发生无法终止情况,看起来像是pthread_cancel将 tid[0] 取消时没有执行pthread_mutex_unlock 函数,这样 mutex 就被永远锁住,线程 tid[1] 也陷入无休止的等待中。事实是这样吗?

2.取消点(Cancellation Point)

  要注意的是 pthread_cancel 调用并不等待线程终止,它只提出请求。线程在取消请求pthread_cancel发出后会继续运行,直到到达某个取消点(Cancellation Point)。取消点是线程检查是否被取消并按照请求进行动作的一个位置。pthread_cancel manual 说以下几个 POSIX 线程函数是取消点:

pthread_join(3)
pthread_cond_wait(3)
pthread_cond_timedwait(3)
pthread_testcancel(3)
sem_wait(3)
sigwait(3)

  以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。在中间我们可以找到 pthread_cond_wait就是取消点之一。

在 APUE 12.7节里 详细介绍了POSIX.1定义的可选取消点。

  但是,令人迷惑不解的是,所有介绍 Cancellation Points 的文章都仅仅说,当线程被取消后,将继续运行到取消点并发生取消动作。但我们注意到上面例子中 pthread_cancel前面 main 函数已经 sleep 了 5 秒,那么在pthread_cancel被调用时,thread0 到底运行到 pthread_cond_wait 没有?如果 thread0 运行到了 pthread_cond_wait,那么照上面的说法,它应该继续运行到下一个取消点并发生取消动作,而后面并没有取消点,所以 thread0 应该运行到 pthread_exit 并结束,这时 mutex 就会被解锁,这样就不应该发生死锁啊。

说明:
从我的GDB中可以看出,运行到pthread_cond_wait这里后,就没有往下运行了。应该说,这是当前的取消点。

3. 取消类型(Cancellation Type)

  我们会发现,通常的说法:某某函数是 Cancellation Points,这种方法是容易令人混淆的。因为函数的执行是一个时间过程,而不是一个时间点。其实真正的 Cancellation Points 只是在这些函数中 Cancellation Type 被修改为 PHREAD_CANCEL_ASYNCHRONOUS 和修改回 PTHREAD_CANCEL_DEFERRED 中间的一段时间。
  POSIX 的取消类型有两种,一种是延迟取消(PTHREAD_CANCEL_DEFERRED),这是系统默认的取消类型,即在线程到达取消点之前,不会出现真正的取消;另外一种是异步取消(PHREAD_CANCEL_ASYNCHRONOUS),使用异步取消时,线程可以在任意时间取消

4. Linux 的取消点实现

  下面我们看 Linux 是如何实现取消点的。(其实这个准确点儿应该说是 GNU 取消点实现,因为 pthread 库是实现在 glibc 中的。) 我们现在在 Linux 下使用的 pthread 库其实被替换成了 NPTL,被包含在 glibc 库中。
pthread_cond_wait 为例,glibc-2.6/nptl/pthread_cond_wait.c 中:
示例代码

/* Enable asynchronous cancellation. Required by the standard. */
cbuffer.oldtype = __pthread_enable_asynccancel ();
148 /* Wait until woken by signal or broadcast. */
lll_futex_wait (&cond->__data.__futex, futex_val);
151 /* Disable asynchronous cancellation. */
__pthread_disable_asynccancel (cbuffer.oldtype);

  我们可以看到,在线程进入等待之前,pthread_cond_wait 先将线程取消类型设置为异步取消(__pthread_enable_asynccancel),当线程被唤醒时,线程取消类型被修改回延迟取消 __pthread_disable_asynccancel
  这就意味着,所有在__pthread_enable_asynccancel 之前接收到的取消请求都会等待__pthread_enable_asynccancel 执行之后进行处理,所有在__pthread_disable_asynccancel之前接收到的请求都会在 __pthread_disable_asynccancel 之前被处理,所以真正的 Cancellation Point 是在这两点之间的一段时间。

也就是在__pthread_enable_asynccancel__pthread_disable_asynccancel间处理取消请求

5. 对示例函数进入死锁的解释

  当main函数中调用 pthread_cancel前,thread0 已经进入了 pthread_cond_wait函数并将自己列入等待条件的线程列表中(lll_futex_wait)。这个可以通过 GDB 在各个函数上设置断点来验证。
  当 pthread_cancel 被调用时,tid[0] 线程仍在等待,取消请求发生在 __pthread_disable_asynccancel前,所以会被立即响应。但是 pthread_cond_wait 为线程注册了一个线程清理程序(glibc-2.6/nptl/pthread_cond_wait.c):

126 /* Before we block we enable cancellation. Therefore we have to
127 install a cancellation handler. */
128 __pthread_cleanup_push (&buffer, __condvar_cleanup, &cbuffer);

  那么这个线程 清理程序 __condvar_cleanup 干了什么事情呢?我们可以注意到在它的实现最后(glibc-2.6/nptl/pthread_cond_wait.c):

85 /* Get the mutex before returning unless asynchronous cancellation
86 is in effect. */
87 __pthread_mutex_cond_lock (cbuffer->mutex);
88}

  哦,__condvar_cleanup 在最后将 mutex (这个mutex 就是调用pthread_cond_wait 的时候传递的 mutex )重新锁上了。而这时候 thread1 还在休眠(sleep(10)),等它醒来时,mutex 将会永远被锁住,这就是为什么 thread1 陷入无休止的阻塞中。

可是为什么pthread_cond_wait要在最后上锁呢?

6. 如何避免因此产生的死锁

  由于线程清理函数 pthread_cleanup_push 使用的策略是先进后出(FILO),那么我们可以在pthread_cond_wait 函数前先注册一个线程处理函数,示例代码:

void cleanup(void *arg)
{
    pthread_mutex_unlock(&mutex);
}
void* thread0(void* arg)
{
    pthread_cleanup_push(cleanup, NULL); // thread cleanup handler
    pthread_mutex_lock(&mutex);
    pthread_cond_wait(&cond, &mutex);
    pthread_mutex_unlock(&mutex);
    pthread_cleanup_pop(0);
    pthread_exit(NULL);
}

  这样,当线程被取消时,先执行 pthread_cond_wait 中注册的线程清理函数 __condvar_cleanup,将 mutex 锁上,再执行 thread0 中注册的线程处理函数 cleanup,将mutex解锁。这样就避免了死锁的发生。

7. 结论

  多线程下的线程同步一直是一个让人很头痛的问题。POSIX 为了避免立即取消程序引起的资源占用问题而引入的 Cancellation Points 概念是一个非常好的设计,但是不合适的使用 pthread_cancel 仍然会引起线程同步的问题。了解POSIX 线程取消点在 Linux 下的实现更有助于理解它的机制和有利于更好的应用这个机制。

``` typedef struct { pthread_mutex_t hLock; volatile int iIsRun; void* (* volatile pfnTask)(void*); //函数指针 void *arg; //任务参数 }Task; typedef struct { pthread_mutex_t StackLock; int iIDStack[MAX_TASK_NUM]; int iTopPointer; }Stack; static Stack g_IDTaskStack; static Task stTaskQueueAry[MAX_TASK_NUM]; static int iTaskCount = 0; //static pthread_mutex_t stMutexQueue; //static pthread_cond_t stCondQueue; static pthread_t stThreadAry[cmnDfn_WORKING_PTHREAD_NUMBER]; void* PthreadPoolWorker(void* arg) { int iRet = -1; int iTskID = (int)(long)arg; Task *pstTsk = &stTaskQueueAry[iTskID]; while(g_iExit == 0) { if (pstTsk->iIsRun == 0) { sleep(1); continue; } iRet = (int)(long)pstTsk->pfnTask(pstTsk->arg); if(iRet < 0) { break; } if (iRet == 0) { continue; } LOCK(pstTsk->hLock) { pstTsk->pfnTask = NULL; pstTsk->iIsRun = 0; } LOCK(g_IDTaskStack.StackLock) { g_IDTaskStack.iIDStack[++g_IDTaskStack.iTopPointer] = iTskID; } } iRet = 0; _Exit: return (void *)(long)iRet; } int ThreadPoolSubmit(void* (*task)(void*),void* arg,int *piTskID) { int iLoopNum; int iTskID = 0; /*for(iLoopNum = 0; iLoopNum < MAX_TASK_NUM;iLoopNum++) { if (stTaskQueueAry[iLoopNum].pfnTask == NULL) { break; } } if(iLoopNum >= MAX_TASK_NUM) { return -1; }*/ if(g_IDTaskStack.iTopPointer < 0) { return -1; } LOCK(g_IDTaskStack.StackLock) { iTskID = g_IDTaskStack.iIDStack[g_IDTaskStack.iTopPointer--]; } LOCK(stTaskQueueAry[iTskID].hLock) { stTaskQueueAry[iTskID].pfnTask = task; stTaskQueueAry[iTskID].arg = arg; stTaskQueueAry[iTskID].iIsRun = 1; *piTskID = iTskID; } return 0; } int ThreadPoolInit(void) { int iLoopNum; int iRet = -1; int iTskID = 0; g_IDTaskStack.iTopPointer = -1; for(iLoopNum = 0; iLoopNum < MAX_TASK_NUM; iLoopNum++) { g_IDTaskStack.iIDStack[++g_IDTaskStack.iTopPointer] = iLoopNum; } for(iLoopNum = 0; iLoopNum < MAX_TASK_NUM; iLoopNum++) { if(pthread_mutex_init(&stTaskQueueAry[iLoopNum].hLock,NULL) < 0) { cmn_PrtError("Error in initializing mutex"); } if(pthread_create(&stThreadAry[iLoopNum],NULL,PthreadPoolWorker,(void *)(long)iTskID) != 0) { cmn_PrtError("Error in creating thread"); } stTaskQueueAry[iLoopNum].pfnTask = NULL; stTaskQueueAry[iLoopNum].iIsRun = 0; iTskID++; } iRet = 0; _Exit: return iRet; } int ThreadPoolDestroy(void) { int iLoopNum; int iRet = -1; g_iExit = 1; for(iLoopNum = 0; iLoopNum < MAX_TASK_NUM; iLoopNum++) { pthread_join(stThreadAry[iLoopNum], NULL); } for(iLoopNum = 0; iLoopNum < MAX_TASK_NUM; iLoopNum++) { pthread_mutex_destroy(&stTaskQueueAry[iLoopNum].hLock); } iRet = 0; return iRet; } #ifndef LOCK #define LOCK(mtx) \ for (int _locked = (pthread_mutex_lock(&(mtx)), 1); \ _locked; \ pthread_mutex_unlock(&(mtx)), _locked = 0) #endif #ifndef cmn_PrtError #define cmn_PrtError(str)\ printf("%s\n", str);\ goto _Exit; #endif```检查代码是否有bug,并给出修改方案
最新发布
03-19
<think>好的,我需要仔细检查用户提供的C语言线程池代码,找出其中的潜在问题,并提出修改建议。首先,我会通读代码,了解整体结构和各个函数的作用。 首先,观察数据结构。Task结构体包含互斥锁hLock、运行状态iIsRun、函数指针pfnTask和参数arg。Stack结构体用于管理任务ID,使用数组iIDStack和栈顶指针iTopPointer。全局变量g_IDTaskStack是任务ID的栈,stTaskQueueAry是任务队列数组,iTaskCount可能记录任务数量,但代码中似乎未使用。工作线程数组stThreadAry和线程池的初始化、销毁函数,以及任务提交函数ThreadPoolSubmit。 接下来,检查线程工作函数PthreadPoolWorker。它接收任务ID,从stTaskQueueAry中获取任务,循环检查是否运行。如果任务未运行,休眠1秒继续。然后执行任务函数,处理返回值,最后释放任务并将ID压回栈。这里可能存在一些问题,比如任务执行时是否持有锁,以及任务状态修改的同步问题。 在ThreadPoolSubmit函数中,从栈中弹出任务ID,设置任务参数并标记为运行。初始化函数ThreadPoolInit填充栈中的任务ID,初始化互斥锁,并创建线程。销毁函数ThreadPoolDestroy等待线程结束并销毁互斥锁。 现在开始逐个分析可能的问题: 1. **iTskID的初始化与传递**:在ThreadPoolInit中,创建线程时传入的iTskID在循环中初始化为0,但循环内iTskID++,而创建线程的参数是(void*)(long)iTskID。这里可能有问题,因为iTskID在每次循环中递增,但传递给每个线程的是当前的iTskID值吗?例如,第一次循环iTskID是0,创建线程后iTskID++变为1。但代码中的iTskID变量在循环中是局部变量,可能在循环体内被正确传递吗? 实际上,代码中的iTskID初始化为0,然后在循环中,每次循环iTskID++。但是在pthread_create调用时,参数是(void*)(long)iTskID。这里可能存在竞争条件,因为iTskID是一个局部变量,可能在循环中被多个线程访问时,其值已经被改变。例如,当创建线程时,传递的是当前的iTskID值,但线程可能不会立即执行,等到实际执行时,iTskID可能已经被增加多次,导致线程获取到错误的ID。正确的做法应该是为每个线程传递对应的循环变量iLoopNum,因为iLoopNum从0到MAX_TASK_NUM-1,而iTskID初始为0,每次循环iTskID++,可能iLoopNum和iTskID是同步递增的。不过,这里可能存在混淆,因为iTskID初始化为0,然后在循环中iLoopNum从0开始,而iTskID在循环体内递增。但实际在循环体内,iTskID++是在每次循环执行完毕后才会执行,因为iTskID++位于循环体的末尾。所以,当iLoopNum从0到MAX_TASK_NUM-1时,iTskID也会从0递增到MAX_TASK_NUM-1,每个线程可能被正确分配不同的iTskID。不过,这里需要确认是否在创建线程时,iTskID的值是正确的。例如,假设iLoopNum循环到i,那么iTskID的值是否等于i?因为iTskID初始为0,循环体内iTskID++,所以第一次循环iLoopNum=0时,iTskID=0,创建线程后iTskID变为1。第二次循环iLoopNum=1时,iTskID已经为1,创建线程后变为2,依此类推。这样,每个线程接收的iTskID参数实际上是iLoopNum的值吗?或者iTskID最终会等于MAX_TASK_NUM?例如,如果MAX_TASK_NUM是5,循环5次,iTskID从0增加到4,然后iLoopNum从0到4。此时,每个线程得到的iTskID参数分别是0,1,2,3,4,这可能是正确的。但需要确保传递的是正确的任务ID,因为每个任务在数组中的索引应该是从0到MAX_TASK_NUM-1。这里可能没有问题,但需要确认代码逻辑是否正确。 不过,更安全的方式可能是在pthread_create时,将参数设置为iLoopNum,而不是iTskID,因为iLoopNum直接对应数组的索引。或者,在循环中,iTskID应该等于iLoopNum。但当前代码中,iTskID初始化为0,然后在循环体内被递增,而iLoopNum从0到MAX_TASK_NUM-1。这样,iTskID的最终值会是MAX_TASK_NUM,而每个线程创建时的参数是iTskID的当前值。例如,当iLoopNum=0时,iTskID=0,传递给线程的参数是0;然后iTskID++到1。iLoopNum=1时,iTskID是1,传递给线程的参数是1,依此类推。因此,每个线程的iTskID参数正确对应数组的索引。这可能没有问题,但代码中的变量命名可能容易引起误解,比如iTskID的初始化和递增是否与iLoopNum同步。例如,如果MAX_TASK_NUM是10,循环10次,iTskID从0增加到9,每个线程得到0到9的参数,这样是正确的。因此,这部分可能没有问题。 2. **任务ID栈的初始化和使用**:在ThreadPoolInit中,初始化g_IDTaskStack时,iTopPointer初始化为-1,然后循环填充iIDStack,从0到MAX_TASK_NUM-1。例如,当MAX_TASK_NUM=5时,栈顶指针从-1开始,每次循环iLoopNum从0到4,执行++g_IDTaskStack.iTopPointer,即iTopPointer变为0,1,2,3,4。栈中的元素是0到4,顺序是0到4。这样,栈顶是4的位置。当提交任务时,从栈顶取出iTskID,即先取出4,然后3,依此类推。这可能没有问题,但任务的处理顺序是否应该是先进后出?这取决于设计,但通常线程池的任务分配可能更倾向于使用队列,但这里使用栈结构可能导致任务分配顺序是LIFO,不过这可能不是问题,具体取决于需求。 3. **ThreadPoolSubmit函数中的任务ID获取**:在提交任务时,如果栈顶指针小于0,返回错误。否则,取出栈顶的iTskID。然后设置任务参数,并将iTskID返回给调用者。这里可能存在一个问题:当取出iTskID后,该任务结构体被重新赋值,但此时对应的线程可能还在运行之前的任务?或者,每个任务结构体与一个线程绑定?比如,每个线程在创建时被分配了一个固定的iTskID,并且一直处理该ID的任务。这样,每个线程对应一个任务结构体,当任务被提交到该结构体时,线程会处理它。但这样设计是否合理?通常线程池中的线程是多个线程处理多个任务,任务被分配到空闲的线程。而这里的实现似乎每个线程对应一个固定的任务结构体,当提交任务时,选择一个空闲的任务槽(通过栈获取ID),然后将任务函数和参数填入该槽,并标记为运行。对应的线程会不断检查自己的任务槽是否有任务,如果有则执行。这种设计可能效率较低,因为每个线程只能处理自己槽中的任务,无法共享任务队列。例如,如果有10个线程,但任务槽被填满后,新任务无法提交,即使有线程空闲。这种设计可能不如传统的线程池使用任务队列,所有线程从同一个队列中获取任务。当前的设计可能导致任务分配不均衡,某些线程可能一直忙碌,而其他线程空闲,因为任务被分配到特定的槽中。 4. **线程工作函数中的循环逻辑**:在PthreadPoolWorker中,每个线程检查自己的任务槽的iIsRun是否为0,如果是则休眠1秒。这可能导致线程在任务未到来时频繁休眠,浪费CPU资源。更好的方式可能是使用条件变量,当任务到来时唤醒对应的线程。例如,当任务被提交到某个槽时,通知对应的线程立即处理,而不是轮询。当前实现中,线程需要不断检查iIsRun的状态,这可能效率低下,尤其是在高并发场景下。 5. **互斥锁的使用**:代码中使用宏LOCK来加锁,解锁。例如,在修改任务结构体或栈时使用锁。然而,在PthreadPoolWorker中,当任务执行完成后,先锁住任务结构体的锁,然后设置pfnTask为NULL,iIsRun为0,接着锁住栈的锁,将任务ID压回栈。这可能正确,但需要注意锁的顺序,避免死锁。例如,如果两个线程同时尝试获取不同顺序的锁,可能导致死锁。但在当前代码中,每个线程在处理自己的任务结构体时,先获取自己的hLock,然后获取StackLock。如果所有线程在修改自己的任务结构体时都是先获取自己的锁,再获取StackLock,那么可能不会出现死锁。但需要确保全局的StackLock的获取顺序在所有情况下都是相同的。 6. **volatile关键字的使用**:在Task结构体中,iIsRun和pfnTask被声明为volatile,这是为了确保线程之间可见性。然而,仅使用volatile可能不足以保证原子性,尤其是在多核处理器上。正确的做法应该是在访问这些变量时使用锁或者原子操作。例如,在检查iIsRun时,是否应该先获取锁?当前代码中,线程在循环中检查iIsRun时没有加锁,这可能导致数据竞争。例如,当主线程在ThreadPoolSubmit中设置iIsRun为1,而工作线程在检查iIsRun时可能看到过期的值。因此,这里存在竞态条件。正确的做法应该是在访问或修改iIsRun时,必须持有对应的hLock。例如,在PthreadPoolWorker中,检查iIsRun之前应该先加锁,或者使用原子操作。 7. **任务函数返回值的处理**:当pfnTask返回iRet<0时,线程退出循环;如果iRet==0,继续循环。这可能意味着任务函数需要返回不同的状态来控制线程的行为。但需要注意,如果任务函数返回0,线程会继续循环,再次检查iIsRun是否为0,可能再次执行任务?或者任务函数是否应该只执行一次?当前的逻辑似乎允许任务函数返回0时,任务被重新执行,但此时iIsRun仍然为1,因为只有在iRet!=0时才会清除pfnTask和iIsRun。这可能与预期不符。例如,如果任务函数返回0,线程会继续循环,再次调用pfnTask,导致任务被重复执行,即使iIsRun仍然为1。这可能是一个错误,因为任务应该执行一次,然后被标记为完成,iIsRun设为0。但当前代码只有在iRet!=0时才会清除任务。因此,如果任务函数返回0,线程会继续执行该任务,这可能不是预期的行为。例如,任务函数可能返回0表示成功,但此时线程会继续循环,再次调用该任务函数,导致无限执行。 8. **iTaskCount变量未使用**:代码中定义了static int iTaskCount = 0; 但该变量在整个代码中没有被使用,可能是残留的无用代码,应该删除。 9. **cmn_PrtError宏中的goto _Exit**:在初始化互斥锁或创建线程失败时,调用cmn_PrtError,该宏会跳转到_Exit标签。但ThreadPoolInit函数中的_Exit标签后面的代码会返回iRet,此时iRet可能未被正确设置。例如,在初始化互斥锁失败时,iRet仍然是-1,这可能正确,但需要确认是否所有错误路径都正确设置了iRet。 10. **线程参数的传递**:在创建线程时,参数是(void*)(long)iTskID,而在PthreadPoolWorker中,参数被转换为(int)(long)arg。在64位系统中,将指针转换为int可能导致截断,因为int通常是32位,而指针是64位。正确的做法是使用uintptr_t类型来转换,或者直接传递指针。例如,可以将iTskID转换为void*,然后在工作线程中转换回uintptr_t,再转为int。当前的转换在32位系统可能没问题,但在64位系统可能导致问题。因此,应该使用正确的类型转换,例如: ```c pthread_create(..., (void*)(uintptr_t)iTskID); ``` 然后在工作线程中: ```c int iTskID = (int)(uintptr_t)arg; ``` 11. **任务ID栈的溢出和下溢**:在ThreadPoolInit中,栈被初始化为MAX_TASK_NUM个任务ID。当提交任务时,如果栈顶指针为-1,返回错误。但在ThreadPoolSubmit中,取出栈顶元素时,使用g_IDTaskStack.iTopPointer--,这可能导致栈顶指针变为-1,如果之后继续提交任务,会正确返回错误。这部分逻辑可能正确,但需要确保在多线程环境下,栈操作是线程安全的,即LOCK(g_IDTaskStack.StackLock)正确包裹了对栈的访问。 12. **线程退出条件**:工作线程的循环条件是g_iExit == 0,但g_iExit变量未在代码中定义,这会导致编译错误。用户提供的代码中可能省略了该变量的定义,但在实际代码中必须定义和初始化,例如: ```c static volatile int g_iExit = 0; ``` 否则,代码无法编译通过。 13. **任务函数指针的检查**:在ThreadPoolSubmit中,注释掉的代码原本是遍历任务数组寻找pfnTask为NULL的槽,但后来改为使用栈来管理空闲ID。这可能没问题,但需要确保在提交任务时,从栈中取出的iTskID对应的任务槽确实处于空闲状态。例如,当任务被提交后,该槽的pfnTask被设置为非NULL,并且在任务完成后被设为NULL。因此,使用栈管理空闲ID是可行的,但需要确保在将ID压回栈时,任务槽已经被正确标记为空闲。 14. **任务执行时的锁持有情况**:在PthreadPoolWorker中,当执行pfnTask时,没有持有任何锁。这可能导致在任务执行期间,任务结构体的其他字段被修改,例如arg被更改。但根据设计,每个任务槽对应一个线程,任务执行期间,其他线程不会修改该槽,因为只有对应的线程会处理该槽。因此,可能不需要在任务执行期间持有锁。但需要确保在任务执行过程中,任务结构体的arg等字段不会被其他线程修改。当前代码中,任务提交时会设置arg,而任务执行时只有对应的线程处理该任务,因此在执行期间arg不会被修改,因为iIsRun在任务执行完成后才会被设为0,此时ThreadPoolSubmit才能重新提交该任务槽。这可能正确,但需要确认。 15. **任务执行失败的处理**:如果pfnTask返回iRet < 0,线程会break退出循环。此时,该线程将不再处理其他任务,而该任务槽的ID可能未被压回栈,导致后续无法再使用该ID。例如,当任务函数返回-1时,线程退出循环,执行LOCK(pstTsk->hLock)部分,设置pfnTask为NULL,iIsRun为0,并将ID压回栈。但如果在break之前没有执行这些操作,那么该任务槽将无法被回收,导致资源泄漏。查看代码: 在PthreadPoolWorker中: ```c iRet = (int)(long)pstTsk->pfnTask(pstTsk->arg); if(iRet < 0) { break; } if (iRet == 0) { continue; } LOCK(pstTsk->hLock) { pstTsk->pfnTask = NULL; pstTsk->iIsRun = 0; } LOCK(g_IDTaskStack.StackLock) { g_IDTaskStack.iIDStack[++g_IDTaskStack.iTopPointer] = iTskID; } ``` 当iRet < 0时,线程直接break,不会执行后续的清理代码。这会导致pfnTask和iIsRun未被重置,任务ID未被压回栈,导致该任务槽无法再被使用,且栈中缺少该ID,导致后续提交任务时可用ID减少,最终可能无法提交新任务。这是严重的问题。 例如,假设某个任务返回-1,线程退出循环,此时pstTsk->pfnTask仍然指向原来的函数,iIsRun仍为1,且任务ID未被压回栈。这样,该任务槽将永远无法被重用,导致内存泄漏和线程池可用资源减少。 正确的处理应该是在break之前,无论任务返回什么,都需要将任务槽重置,并将ID压回栈。或者,在break之前执行清理操作。 修改建议:在break之前,需要获取锁,重置任务槽,并压回ID。例如: ```c if(iRet < 0) { LOCK(pstTsk->hLock) { pstTsk->pfnTask = NULL; pstTsk->iIsRun = 0; } LOCK(g_IDTaskStack.StackLock) { g_IDTaskStack.iIDStack[++g_IDTaskStack.iTopPointer] = iTskID; } break; } ``` 但需要确保在break前执行这些操作。 16. **线程函数中的sleep(1)**:当任务槽的iIsRun为0时,线程sleep(1),这会导致延迟,降低响应速度。更好的做法是使用条件变量等待,当有任务提交时通知线程。当前实现中,线程会不断轮询,可能导致CPU资源浪费。 17. **线程池销毁函数ThreadPoolDestroy**:设置g_iExit=1,然后等待所有线程结束。但工作线程的循环条件是g_iExit == 0,因此当g_iExit变为1时,所有线程会退出循环,正确结束。这部分逻辑正确,但需要确保g_iExit是volatile修饰,并且所有线程都能看到其值的改变。当前代码中g_iExit可能未声明为volatile,这可能导致线程无法及时看到g_iExit的变化。因此,应该将g_iExit声明为volatile: ```c static volatile int g_iExit = 0; ``` 18. **宏LOCK的定义**:使用for循环实现锁的获取和释放,这可能导致某些编译器优化问题,但通常可以工作。不过,更常见的方式是使用pthread_mutex_lock和pthread_mutex_unlock,但当前宏定义是正确的。 19. **错误处理中的资源释放**:在ThreadPoolInit中,如果pthread_mutex_init或pthread_create失败,跳转到_Exit,但此时可能已经初始化了一些互斥锁或创建了一些线程,需要在这些情况下进行清理。例如,如果在循环中部分互斥锁初始化成功,但后续失败,应该销毁已经初始化的互斥锁,并join已经创建的线程。当前的代码在错误处理中直接跳转,没有回滚操作,可能导致资源泄漏。 20. **任务函数指针的volatile修饰**:在Task结构体中,pfnTask被声明为volatile函数指针。volatile关键字告诉编译器不要优化该变量,每次访问都从内存中读取。这在多线程环境中可能是必要的,但正确的方式应该是在访问该变量时使用锁,而不是依赖volatile。因为volatile不能保证原子性,只能保证可见性。例如,当线程A修改pfnTask时,线程B可能在无锁的情况下读取到中间状态。因此,正确的做法是在访问pfnTask时始终持有对应的hLock。 综合以上分析,主要问题包括: 1. **任务函数返回负值时的资源泄漏**:当任务返回iRet < 0时,线程直接break,未重置任务槽和压回ID,导致任务槽无法重用。 2. **iIsRun和pfnTask的竞态条件**:访问这些变量时未加锁,可能导致数据不一致。 3. **线程参数的截断问题**:在64位系统上,将指针转换为int可能导致截断。 4. **g_iExit变量未声明**:代码中未定义g_iExit,导致编译错误。 5. **线程休眠效率低下**:使用sleep(1)导致延迟,应使用条件变量。 6. **错误处理中资源泄漏**:初始化失败时未回滚已分配的资源。 7. **任务重复执行问题**:当任务返回0时,线程会继续执行该任务,可能导致无限循环。 修改方案如下: 1. **处理任务返回负值的情况**:在break前重置任务槽并将ID压回栈。 2. **在访问iIsRun和pfnTask时加锁**:确保每次访问这些变量都持有对应的锁。 3. **使用正确的类型转换**:避免指针截断,使用uintptr_t。 4. **定义g_iExit并声明为volatile**。 5. **使用条件变量替代轮询**:提高效率,减少CPU占用。 6. **修复错误处理中的资源释放**。 7. **修改任务返回0时的逻辑**,避免重复执行任务。 例如,针对任务返回负值的修改: 在PthreadPoolWorker中: ```c if(iRet < 0) { LOCK(pstTsk->hLock) { pstTsk->pfnTask = NULL; pstTsk->iIsRun = 0; } LOCK(g_IDTaskStack.StackLock) { g_IDTaskStack.iIDStack[++g_IDTaskStack.iTopPointer] = iTskID; } break; } ``` 针对竞态条件,修改PthreadPoolWorker中的循环: 在检查iIsRun时,必须持有锁: ```c while(g_iExit == 0) { int isRun = 0; LOCK(pstTsk->hLock) { isRun = pstTsk->iIsRun; } if (!isRun) { // 使用条件变量等待 sleep(1); continue; } // 执行任务... } ``` 但更好的方式是使用条件变量来通知线程任务到达,而不是轮询。这需要修改代码结构,引入条件变量。 由于改动较大,可能需要重新设计任务队列的管理方式,使用条件变量和更高效的同步机制。但根据当前代码结构,可以优先解决已发现的明显问题。</think>以下是对代码的检查及修改建议: --- ### 一、主要问题分析 #### 1. **任务异常返回导致资源泄漏** - **问题**:当任务函数返回 `iRet < 0` 时,线程直接退出循环,未重置任务状态和回收任务ID,导致该任务槽永久不可用。 - **修改方案**:在 `break` 前添加清理逻辑: ```c if(iRet < 0) { LOCK(pstTsk->hLock) { pstTsk->pfnTask = NULL; pstTsk->iIsRun = 0; } LOCK(g_IDTaskStack.StackLock) { g_IDTaskStack.iIDStack[++g_IDTaskStack.iTopPointer] = iTskID; } break; } ``` #### 2. **线程参数截断问题** - **问题**:`iTskID` 通过 `(void*)(long)` 强制转换可能丢失精度(64位系统)。 - **修改方案**:使用 `uintptr_t` 保证安全转换: ```c // 线程创建 pthread_create(..., (void*)(uintptr_t)iTskID); // 线程函数中 int iTskID = (int)(uintptr_t)arg; ``` #### 3. **全局退出标志未定义** - **问题**:`g_iExit` 变量未声明,导致编译错误。 - **修改方案**:添加定义并声明为 `volatile`: ```c static volatile int g_iExit = 0; ``` #### 4. **任务状态竞态条件** - **问题**:`iIsRun` 和 `pfnTask` 的访问未加锁,可能导致数据不一致。 - **修改方案**:所有读写操作需在锁保护下进行: ```c // 检查 iIsRun 时加锁 LOCK(pstTsk->hLock) { if (pstTsk->iIsRun == 0) { // 等待逻辑 } } ``` #### 5. **低效的轮询机制** - **问题**:通过 `sleep(1)` 轮询任务状态,响应延迟高。 - **修改方案**:引入条件变量优化等待逻辑(需调整设计)。 --- ### 二、其他改进建议 #### 1. **删除无用变量** - **问题**:`iTaskCount` 定义后未使用。 - **修改方案**:删除 `static int iTaskCount = 0;`。 #### 2. **错误处理不完善** - **问题**:初始化失败时未回滚已分配资源。 - **修改方案**:在 `ThreadPoolInit` 的错误处理中增加清理逻辑: ```c for (int j = 0; j < iLoopNum; j++) { pthread_mutex_destroy(&stTaskQueueAry[j].hLock); pthread_cancel(stThreadAry[j]); } ``` #### 3. **任务重复执行问题** - **问题**:当任务返回 `0` 时,线程会无限重复执行。 - **修改方案**:无论返回值如何,任务执行后均重置状态: ```c iRet = (int)(long)pstTsk->pfnTask(pstTsk->arg); LOCK(pstTsk->hLock) { pstTsk->pfnTask = NULL; pstTsk->iIsRun = 0; } ``` --- ### 三、关键代码修改示例 #### 1. 修复任务异常退出逻辑 ```c void* PthreadPoolWorker(void* arg) { // ... iRet = (int)(long)pstTsk->pfnTask(pstTsk->arg); if (iRet < 0) { LOCK(pstTsk->hLock) { pstTsk->pfnTask = NULL; pstTsk->iIsRun = 0; } LOCK(g_IDTaskStack.StackLock) { g_IDTaskStack.iIDStack[++g_IDTaskStack.iTopPointer] = iTskID; } break; } // ... } ``` #### 2. 线程参数安全转换 ```c // ThreadPoolInit 中 pthread_create(..., (void*)(uintptr_t)iLoopNum); // 直接传递循环变量 // PthreadPoolWorker 中 int iTskID = (int)(uintptr_t)arg; ``` --- ### 四、总结 - **核心问题**:同步机制不完善、资源泄漏、类型安全问题。 - **改进方向**:使用条件变量替代轮询、完善错误处理、保证原子操作。 - **测试建议**:通过高并发场景测试线程池的稳定性和性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值