C语言中pthread_cond_wait 详解

一、pthread_cond_wait函数的原理

  1. pthread_cond_wait()
    用于阻塞当前线程,等待别的线程使用pthread_cond_signal()pthread_cond_broadcast来唤醒它 pthread_cond_wait() 必须与pthread_mutex配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread_cond_signal()pthread_cond_broadcast,把该线程唤醒,使pthread_cond_wait()通过(返回)时,该线程又自动获得该mutex
  2. pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
    使用pthread_cond_signal一般不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。
  3. 但是pthread_cond_signal在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续
    wait,而且规范要求pthread_cond_signal至少唤醒一个pthread_cond_wait上的线程,其实有些实现为了简单在单处理器上也会唤醒多个线程. 另外,某些应用,如线程池,pthread_cond_broadcast唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait.所以强烈推荐对pthread_cond_wait()
    使用while循环来做条件判断.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

//链表的结点
struct msg
{
    int num; 
    struct msg *next; 
};
 
struct msg *head = NULL;    //头指针
struct msg *temp = NULL;    //节点指针

//静态方式初始化互斥锁和条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_producer = PTHREAD_COND_INITIALIZER;
 
void *producer(void *arg)
{
    while (1)   //线程正常不会解锁,除非收到终止信号
	{
        pthread_mutex_lock(&mutex);         //加锁

        temp = malloc(sizeof(struct msg));
        temp->num = rand() % 100 + 1;
        temp->next = head;
        head = temp;                        //头插法
        printf("---producered---%d\n", temp->num);

        pthread_mutex_unlock(&mutex);       //解锁
        pthread_cond_signal(&has_producer); //唤醒消费者线程
        usleep(rand() % 3000);              //为了使该线程放弃cpu,让结果看起来更加明显。
    }
 
    return NULL;
}
 
void *consumer(void *arg)
{
    while (1)       //线程正常不会解锁,除非收到终止信号
	{
        pthread_mutex_lock(&mutex);     //加锁
        while (head == NULL)            //如果共享区域没有数据,则解锁并等待条件变量
	    {
            pthread_cond_wait(&has_producer, &mutex);   //我们通常在一个循环内使用该函数
        }
        temp = head;
        head = temp->next;
        printf("------------------consumer--%d\n", temp->num);
        free(temp);                     //删除节点,头删法
        temp = NULL;                    //防止野指针
        pthread_mutex_unlock(&mutex);   //解锁

        usleep(rand() % 3000);          //为了使该线程放弃cpu,让结果看起来更加明显。
    }
 
    return NULL;
}
 
int main(void)
{
    pthread_t ptid, ctid;
    srand(time(NULL));      //根据时间摇一个随机数种子

    //创建生产者和消费者线程
    pthread_create(&ptid, NULL, producer, NULL);
    pthread_create(&ctid, NULL, consumer, NULL);

    //主线程回收两个子线程
    pthread_join(ptid, NULL);
    pthread_join(ctid, NULL);
 
    return 0;
}
``` typedef struct { pthread_mutex_t hLock; volatile int iIsRun; volatile void* (*pfnTask)(void*); //函数指针 void *arg; //任务参数 }Task; 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; } if(pthread_mutex_lock(&pstTsk->hLock) != 0) { cmn_PrtError("lock failed"); break; } pstTsk->pfnTask = NULL; pstTsk->iIsRun = 0; pthread_mutex_unlock(&pstTsk->hLock); } iRet = 0; _Exit: return (void *)(long)iRet; } int ThreadPoolSubmit(void* (*task)(void),void* arg,int *piTskID) { int iLoopNum; for(iLoopNum = 0; iLoopNum < MAX_TASK_NUM;iLoopNum++) { if (stTaskQueueAry[iLoopNum].pfnTask == NULL) { break; } } if(iLoopNum >= MAX_TASK_NUM) { return -1; } stTaskQueueAry[iLoopNum].pfnTask = task; stTaskQueueAry[iLoopNum].arg = arg; stTaskQueueAry[iLoopNum].iIsRun = 1; *piTskID = iLoopNum; return 0; } int ThreadPoolInit(void) { int iLoopNum; int iRet = -1; int iTskID = 0; for(iLoopNum = 0; iLoopNum < cmnDfn_WORKING_PTHREAD_NUMBER; iLoopNum++) { if(pthread_mutex_init(&stTaskQueueAry[iLoopNum].hLock,NULL) < 0) { cmn_PrtError("Error in initializing mutex"); } if(Pthread_create(&stTaskQueueAry[iLoopNum],NULL,PthreadPoolWorker,(void *)(long)iTskID) != 0) { cmn_PrtError("Error in creating thread"); } iRet = 0; stTaskQueueAry[iLoopNum].pfnTask = NULL; stTaskQueueAry[iLoopNum].iIsRun = 0; } _Exit: return iRet; } int ThreadPoolDestroy(void) { int iLoopNum; int iRet = -1; g_iExit = 1; for(iLoopNum = 0; iLoopNum < cmnDfn_WORKING_PTHREAD_NUMBER; iLoopNum++) { pthread_join(stThreadAry[iLoopNum], NULL); } for(iLoopNum = 0; iLoopNum < cmnDfn_WORKING_PTHREAD_NUMBER; iLoopNum++) { pthread_mutex_destroy(&stTaskQueueAry[iLoopNum].hLock); } iRet = 0; }```详细解释代码实现功能,运行逻辑并运行过程图
03-15
<think>好的,我现在需要详细解释用户提供的这段C语言线程池代码。首先,我要通读代码,理解各个部分的功能和运行逻辑。看起来这是一个简单的线程池实现,可能用于管理多个工作线程处理任务队列。让我逐步分析。 首先,用户的结构体定义Task,里面有互斥锁hLock,两个volatile变量iIsRun和函数指针pfnTask,以及参数arg。这说明每个任务可能有自己的锁和状态标志。接下来,全局的Task数组stTaskQueueAry和任务计数iTaskCount,可能用于存储所有任务。不过注意到MAX_TASK_NUM应该是任务队列的最大数量,而cmnDfn_WORKING_PTHREAD_NUMBER是线程池中的线程数量。 然后看PthreadPoolWorker函数,这是工作线程的入口函数。参数是iTskID,每个线程对应一个任务结构?这里可能每个线程处理一个特定的任务槽?不太确定。循环中检查iIsRun是否为0,如果是的话就睡眠,否则执行任务函数。执行完成后,锁定互斥锁,重置pfnTask和iIsRun。这里似乎每个任务结构对应一个线程,而不是传统的线程池模型,传统上线程会从队列中获取任务。这可能需要进一步理解。 ThreadPoolSubmit函数用于提交任务。遍历任务队列,找到pfnTask为NULL的位置,填充任务和参数,设置iIsRun为1,并返回任务ID。这说明任务队列是预分配的数组,每个槽位存放一个任务。当提交任务时,找到空闲槽位,分配任务。 ThreadPoolInit函数初始化线程池,创建多个线程,每个线程对应一个任务结构,并初始化互斥锁。这里可能每个线程负责处理自己对应的任务槽,而不是共享任务队列。这有点不同于常规的线程池设计,常规的是多个线程共同处理一个任务队列,通过条件变量等待任务到来。但此代码中没有看到条件变量,而是在工作线程中轮询iIsRun的状态。这可能效率较低,因为线程需要不断检查状态。 ThreadPoolDestroy设置g_iExit为1,让工作线程退出,然后等待线程结束,销毁互斥锁。 现在,我需要理清楚整个流程。可能的工作流程是: 1. 初始化时,创建多个线程,每个线程绑定到任务数组中的一个元素,初始化各自的互斥锁,并将pfnTask设为NULL,iIsRun设为0。 2. 当提交任务时,找到一个空闲的任务槽(pfnTask为NULL的位置),填入任务函数和参数,设置iIsRun为1。对应的线程在循环中发现iIsRun为1,则执行任务。 3. 工作线程执行完任务后,将pfnTask置为NULL,iIsRun置为0,释放锁,回到循环等待。 这种设计下,每个线程负责处理自己槽位中的任务。当有任务提交到某个槽位,对应的线程就会处理它。这类似于每个线程有一个专属的任务槽,而不是共享的任务队列。这样的设计可能适用于任务之间相互独立,且任务数量不超过线程数的情况。但问题在于,如果任务数超过线程数,就无法处理,因为每个线程只能处理自己槽位的任务。而传统的线程池中,任务队列可以被所有线程竞争获取,处理更多的任务。 此外,代码中可能存在的错误或需要注意的地方: - 在ThreadPoolInit中的Pthread_create调用,可能应该是pthread_create,但原代码中可能是一个封装函数,比如Pthread_create可能是一个宏或自定义函数,用于错误处理。原代码中的stThreadAry可能未被正确赋值,因为在线程创建时,参数是&stTaskQueueAry[iLoopNum]吗?或者原代码中的Pthread_create的参数可能有误。比如,pthread_create的第二个参数是线程属性,第三个是函数指针,第四个是参数。原代码中的Pthread_create可能将线程ID存储到stThreadAry中,但在当前代码中,stThreadAry未被使用,可能在线程创建时没有正确保存线程ID,导致后续pthread_join无法正确执行。例如,ThreadPoolInit中创建线程时,可能应该将线程ID存入stThreadAry[iLoopNum],但当前代码中的Pthread_create调用可能没有这样做。例如,pthread_create的正确用法是: pthread_create(&stThreadAry[iLoopNum], NULL, PthreadPoolWorker, ...); 但原代码中的调用是: Pthread_create(&stTaskQueueAry[iLoopNum], ...) 这可能存在错误,因为pthread_create的第一个参数应该是pthread_t指针,用来存储新线程的ID,而原代码传入的是Task结构体的指针,这显然不正确。因此,这里的代码可能存在严重错误,导致线程ID没有被正确保存,导致后续销毁时pthread_join无法正常工作。这可能是代码中的bug。 另外,在PthreadPoolWorker函数中,iTskID是作为参数传入的,每个线程处理一个特定的任务槽。假设cmnDfn_WORKING_PTHREAD_NUMBER等于MAX_TASK_NUM,否则可能会有越界访问。例如,如果工作线程数量超过任务队列大小,可能会访问到无效的数组索引。 此外,ThreadPoolSubmit中的循环查找空闲任务槽,当所有槽都被占用时返回-1。这可能存在并发问题,因为多个线程可能同时调用Submit,导致竞争条件。但原代码中没有使用互斥锁保护这个查找过程,可能导致两个线程找到同一个空闲槽,进而导致任务覆盖或其他错误。因此,这里的线程安全性存在问题。 另外,在任务执行时,当pstTsk->iIsRun被设置为1后,工作线程开始执行任务。但是,这里可能存在可见性问题,因为iIsRun是volatile的,这可能确保线程能及时看到改变,但任务参数的传递是否有同步措施?比如,在Submit函数中设置arg和pfnTask之后,设置iIsRun为1,此时工作线程可能立即开始执行,但此时是否已经正确设置了参数?这里可能没有足够的同步机制,需要确保在设置iIsRun之前,其他字段已经正确写入,可能通过内存屏障或者锁来保证。但原代码中,在Submit函数中没有加锁,可能导致工作线程看到部分更新的数据。 另外,在任务执行完成后,工作线程会加锁,然后重置pfnTask和iIsRun。这里可能正确,因为工作线程在修改自己的任务结构,而Submit函数在寻找空闲槽时检查pfnTask是否为NULL。但如果有多个线程同时调用Submit,可能会存在竞争,比如两个Submit线程同时找到同一个iLoopNum,导致任务被覆盖。 此外,ThreadPoolInit函数中的循环次数是cmnDfn_WORKING_PTHREAD_NUMBER次,初始化每个线程对应的任务结构,并创建线程。假设cmnDfn_WORKING_PTHREAD_NUMBER等于MAX_TASK_NUM,否则可能会出现数组越界。或者,可能每个线程对应一个任务结构,即线程数量和任务数量相同,这样每个线程处理一个任务槽。这可能限制了线程池的灵活性,因为任务数量不能超过线程数量,无法有效处理大量任务,只能处理与线程数相同的并发任务。 综上所述,这个线程池的实现可能是一个固定数量的任务槽,每个任务槽由一个专用线程处理。任务提交时找到空闲槽,线程轮询自己的槽位是否有任务。这种方式可能效率较低,因为线程需要不断轮询iIsRun的状态,而不是通过条件变量等待通知。同时,线程安全性方面存在潜在问题,如Submit函数的竞争条件,线程ID存储错误等。 接下来,我需要将这些分析整理成用户容易理解的步骤,解释代码的功能、运行逻辑,并指出可能的运行流程图。</think>以下是对代码的详细分析: ### 一、代码功能概述 这是一个**固定任务槽的线程池实现**,核心设计是**每个工作线程绑定一个独立任务槽**。任务提交时寻找空闲槽位,线程轮询检查自己的槽位任务状态。适用于**任务数量有限且无需动态扩展**的场景。 --- ### 二、关键数据结构 ```c typedef struct { pthread_mutex_t hLock; // 任务专用互斥锁 volatile int iIsRun; // 任务运行标志(1=可执行) volatile void* (*pfnTask)(void*); // 任务函数指针 void *arg; // 任务参数 } Task; static Task stTaskQueueAry[MAX_TASK_NUM]; // 固定任务槽数组 static pthread_t stThreadAry[WORKER_THREAD_NUM]; // 工作线程ID数组 ``` --- ### 三、运行逻辑详解 #### 1. 初始化阶段 (`ThreadPoolInit`) - **循环创建线程**:创建 `WORKER_THREAD_NUM` 个工作线程。 - **绑定任务槽**:每个线程通过 `iTskID` 绑定到 `stTaskQueueAry` 的特定槽位。 - **初始化锁和状态**:为每个任务槽初始化互斥锁,并重置任务状态。 - ❗潜在问题:原代码中 `pthread_create` 调用错误地将 `Task` 结构指针作为线程ID存储位置,正确做法应使用 `stThreadAry` 存储线程ID。 #### 2. 提交任务 (`ThreadPoolSubmit`) - **查找空闲槽**:遍历数组寻找 `pfnTask == NULL` 的槽位。 - **填充任务数据**:设置任务函数、参数并激活 `iIsRun` 标志。 - ❗线程安全问题:未加锁可能导致多线程提交时找到相同空闲槽。 #### 3. 工作线程流程 (`PthreadPoolWorker`) ```plaintext +-----------------------+ | 检查 g_iExit 是否为1 | +-----------|-----------+ | 否 v +-----------------------+ | 检查 iIsRun 是否为1 | +-----------|-----------+ | 否 v sleep(1) → 轮询等待任务 | | 是 v +-----------------------+ | 执行 pfnTask(arg) | +-----------|-----------+ | v +-----------------------+ | 加锁并重置任务状态 | | (pfnTask=NULL, iIsRun=0) +-----------------------+ ``` #### 4. 销毁阶段 (`ThreadPoolDestroy`) - **通知线程退出**:设置全局标志 `g_iExit = 1`。 - **回收线程资源**:通过 `pthread_join` 等待所有线程结束。 - **销毁互斥锁**:释放每个任务槽的锁资源。 --- ### 四、运行过程示意图 ```plaintext [主线程] | | ThreadPoolSubmit() v +---------------------+ | 遍历任务槽数组 | | 找到空闲槽位iLoopNum | +---------------------+ | v +---------------------+ | 填充任务数据 | | (pfnTask, arg, iIsRun=1) +---------------------+ [工作线程i] | v +---------------------+ | 轮询检查iIsRun标志 | +----------+----------+ | +---------+---------+ | 是 | 否 v v +------------------+ +------------+ | 执行任务函数 | | sleep(1) | | pfnTask(arg) | +-----+------+ +------------------+ | | | v | +------------------+ | | 加锁重置任务状态 | <---------+ +------------------+ ``` --- ### 五、代码缺陷与改进建议 1. **线程安全问题**: - `ThreadPoolSubmit` 未加锁可能导致任务覆盖。 - **改进**:使用全局锁保护任务队列遍历过程。 2. **低效轮询机制**: - 工作线程通过 `sleep(1)` 轮询检查状态。 - **改进**:使用条件变量 (`pthread_cond_t`) 实现事件驱动。 3. **线程ID存储错误**: - 原代码错误地将 `Task` 结构指针作为线程ID存储位置。 - **修正**:正确使用 `stThreadAry` 存储线程ID。 4. **任务与线程1:1绑定**: - 任务数量受限于线程数,无法处理高并发。 - **改进**:实现共享任务队列+动态任务分配。 --- ### 六、关键公式说明 1. **任务槽利用率公式**: $$ \text{利用率} = \frac{\text{活跃任务数}}{\text{MAX_TASK_NUM}} \times 100\% $$ 当所有槽位被占满时 (`iLoopNum >= MAX_TASK_NUM`),新任务提交会失败。 2. **线程唤醒延迟**: 由于使用 `sleep(1)` 轮询,最大任务响应延迟为: $$ \text{最大延迟} = 1\text{s} + \text{任务执行时间} $$ --- 该实现适合**低并发固定任务场景**,如需高性能线程池建议参考 `pthread_cond_wait` + 任务队列的标准设计模式。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值