有时我们会需要大量线程来处理一些相互独立的任务,为了避免频繁的申请释放线程所带来的开销,我们可以使用线程池。下面是一个C语言实现的简单的线程池。

头文件:

 #ifndef THREAD_POOL_H__
#define THREAD_POOL_H__

#include <pthread.h>

/* 要执行的任务链表 */函数指针。名字叫 routine,这个函数的返回值是void *(一个指针),参数是void *(一个指针)

typedef struct tpool_work
{
void *(*routine)(void *); /* 任务函数 */
void *arg; /* 传入任务函数的参数 */
struct tpool_work *next;
} tpool_work_t;

/*
用typedef定义函数指针类型,
typedef char (*PTRFUN)(int);
PTRFUN pFun;
typedef的功能是定义新的类型。第一句就是定义了一种PTRFUN的类型,并定义这种类型为指向某种函数的指针,这种函数以一个int为参数并返回char类型。后面就可以像使用int,char一样使用PTRFUN了。*/

typedef struct tpool
{
int shutdown; /* 线程池是否销毁 */
int max_thr_num; /* 最大线程数 */
pthread_t *thr_id; /* 线程ID数组 */
tpool_work_t *queue_head; /* 线程链表 */
pthread_mutex_t queue_lock;
pthread_cond_t queue_ready;
} tpool_t;
//(何时互斥锁不够,还需要条件变量?)
/*
* @brief 创建线程池
* @param max_thr_num 最大线程数
* @return 0: 成功 其他: 失败
*/
int
tpool_create(int max_thr_num);

/*
* @brief 销毁线程池
*/
void
tpool_destroy();

/*
* @brief 向线程池中添加任务
* @param routine 任务函数指针
* @param arg 任务函数参数
* @return 0: 成功 其他:失败
*/
int
tpool_add_work(void *(*routine)(void *), void *arg);

#endif

实现:

#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>

#include "tpool.h"

static tpool_t *tpool = NULL;

/* 工作者线程函数, 从任务链表中取出任务并执行 */
static void *
thread_routine(void *arg)
{
tpool_work_t *work;

while(1)
{
/* 如果线程池没有被销毁且没有任务要执行,则等待 */
pthread_mutex_lock(&tpool->queue_lock);
while(!tpool->queue_head && !tpool->shutdown) //任务为空或者被销毁了
{
pthread_cond_wait(&tpool->queue_ready, &tpool->queue_lock);
}
if (tpool->shutdown)
{
pthread_mutex_unlock(&tpool->queue_lock);
pthread_exit(NULL);
}
work = tpool->queue_head;
tpool->queue_head = tpool->queue_head->next;
pthread_mutex_unlock(&tpool->queue_lock);

work->routine(work->arg);
free(work);
}

return NULL;
}

/*
* 创建线程池
*/
int
tpool_create(int max_thr_num)
{
int i;

tpool = calloc(1, sizeof(tpool_t));
if (!tpool)
{
printf("%s: calloc failed\n", __FUNCTION__);
exit(1);
}

/* 初始化 */
tpool->max_thr_num = max_thr_num;
tpool->shutdown = 0;
tpool->queue_head = NULL;
if (pthread_mutex_init(&tpool->queue_lock, NULL) != 0)
{
printf("%s: pthread_mutex_init failed, errno:%d, error:%s\n",
__FUNCTION__, errno, strerror(errno));
exit(1);
}
if (pthread_cond_init(&tpool->queue_ready, NULL) != 0 )
{
printf("%s: pthread_cond_init failed, errno:%d, error:%s\n",
__FUNCTION__, errno, strerror(errno));
exit(1);
}

/* 创建工作者线程 */
tpool->thr_id = calloc(max_thr_num, sizeof(pthread_t));
/*calloc 在内存的动态存储区中分配n个长度为size的连续空间,函数返回一个指向分配起始地址的指针;如果分配不成功,返回NULL。*/
if (!tpool->thr_id)
{
printf("%s: calloc failed\n", __FUNCTION__);
exit(1);
}
for (i = 0; i < max_thr_num; ++i)
{
if (pthread_create(&tpool->thr_id[i], NULL, thread_routine, NULL) != 0)
{
printf("%s:pthread_create failed, errno:%d, error:%s\n", __FUNCTION__,
errno, strerror(errno));
exit(1);
}

}

return 0;
}

/* 销毁线程池 */
void
tpool_destroy()
{
int i;
tpool_work_t *member;

if (tpool->shutdown)
{
return;
}
tpool->shutdown = 1;

/* 通知所有正在等待的线程 */
pthread_mutex_lock(&tpool->queue_lock);
pthread_cond_broadcast(&tpool->queue_ready);
pthread_mutex_unlock(&tpool->queue_lock);
for (i = 0; i < tpool->max_thr_num; ++i)
{
pthread_join(tpool->thr_id[i], NULL);
}
free(tpool->thr_id);
while(tpool->queue_head)
{
member = tpool->queue_head;
tpool->queue_head = tpool->queue_head->next;
free(member);
}

pthread_mutex_destroy(&tpool->queue_lock);
pthread_cond_destroy(&tpool->queue_ready);

free(tpool);
}

/* 向线程池添加任务 */
int
tpool_add_work(void *(*routine)(void *), void *arg)
{
tpool_work_t *work, *member;

if (!routine)
{
printf("%s:Invalid argument\n", __FUNCTION__);
return -1;
}

work = malloc(sizeof(tpool_work_t));
if (!work)
{
printf("%s:malloc failed\n", __FUNCTION__);
return -1;
}
work->routine = routine;
work->arg = arg;
work->next = NULL;

pthread_mutex_lock(&tpool->queue_lock);
member = tpool->queue_head;
if (!member)
{
tpool->queue_head = work;
}
else
{
while(member->next)
{
member = member->next;
}
member->next = work;
}
/* 通知工作者线程,有新任务添加 */
pthread_cond_signal(&tpool->queue_ready);
pthread_mutex_unlock(&tpool->queue_lock);

return 0;
}


测试代码:

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

void *func(void *arg)
{
printf("thread %d\n", (int)arg);
return NULL;
}

int
main(int arg, char **argv)
{
if (tpool_create(5) != 0)
{
printf("tpool_create failed\n");
exit(1);
}

int i;
for (i = 0; i < 10; ++i)
{
tpool_add_work(func, (void *)i);
}
sleep(2);
tpool_destroy();
return 0;
}

这个实现是在调用tpool_destroy之后,仅将当前正在执行的任务完成之后就会退出,我们也可以修改代码使得线程池在执行完任务链表中所有任务后再退出。

 

 

相关函数原型:

pthread_cond_wait和pthread_cond_timedwait

pthread_cond_wait和pthread_cond_timedwait用来等待条件变量被设置,值得注意的是这两个等待调用需要一个已经上锁的互斥体mutex,这是为了防止在真正进入等待状态之前别的线程有可能设置该条件变量而产生竞争。pthread_cond_wait的函数原型为: 

pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);

 

三:等待和触发

1条件等待

int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex);

2时间等待

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

 

 其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,

其中abstime以与time()​​系统调用​​相同意义的绝对时间形式出现,0表示​​格林尼治时间​​1970年1月1日0时0分0秒。

 

无论哪种等待方式,都必须和一个​​互斥锁​​配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)

的​​竞争条件​​(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)

或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本​​线程​​加锁(pthread_mutex_lock()),

而在更新条件​​等待队列​​以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。

在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

 

使用pthread_cond_wait方式如下:

    pthread _mutex_lock(&mutex)

    while或if(线程执行的条件是否成立)

          pthread_cond_wait(&cond, &mutex);

    线程执行

    pthread_mutex_unlock(&mutex);

 

相关知识:

- 互斥锁基础知识

- 条件变量基础知识

- 条件变量与互斥锁的运用

- *pthread_cond_wait理解

- 异步信号

- 信号量

互斥锁

互斥锁函数:

pthread_mutex_init 加锁前初始化

pthread_mutex_destroy 解锁完成后释放互斥锁占用资源

pthread_mutex_lock 加锁

pthread_mutex_unlock 解锁

pthread_mutex_tylock 测试加锁

互斥锁就是通过锁的机制,让在同一时刻只允许一个线程执行一个关键的代码,确保一次性执行完毕,不被中断。

条件变量

条件变量函数:

pthread_cond_init 初始化

pthread_cond_wait 基于条件变量阻塞,无条件等等

pthread_cond_timewait 阻塞直到指定事件发生,一段时间等待

pthread_cond_signal 发出激活信号

pthread_cond_broadcast 解除线程所有的阻塞

pthread_cond_destroy 清楚条件变量

条件变量两个操作:首先一个等待使用资源的线程等待条件变量被设置为真,第二个操作就是另一个线程使用完那块公共资源后,把条件设置为真。

条件变量与互斥锁的关系

要保证变量正确被修改,条件变量也需要受到保护,此时用了互斥锁来保护。如果不使用两者搭配的话,当一个线程只使用互斥锁的话,当其他线程到这个程序的时候就会来判断是否上锁,若上锁了就等待解锁,这个时间里可能会有很多线程来判断,然后阻塞在这里,当调用锁的线程释放锁后,那么被阻塞的线程又会来抢夺这个资源,从而造成了资源时间空间的浪费。而当加上了条件变量的时候,被阻塞的线程就会变得有序,在一个队列里排队,当接受到激活信号之前不会反复检查是否该锁已经释放,只需要一个通知(激活信号)来告诉这个线程,你所需要的那个锁已经释放了,现在你可以用了,此时整个过程就变的有序,而不是因为抢夺来消耗资源了。二者的关系操作可以形象看作从一个无序的占用资源变成一个有序的节约资源的一个改变,所以条件变量和互斥锁二者使用能够保证条件变量的正确修改。

pthread_cond_wait理解

在条件变量里面,锁的函数很重要,其中起到关键作用之一的函数便是这个等待函数吧。

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待”条件变量的条件成立”而挂起;另一个线程使”条件成立”(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

—pthread_cond_wait百度百科

函数原型:

int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex)

该函数的两个动作:首先释放由第二个参数指向的锁,解锁后,把第一个参数cond的条件变量指向条件阻塞变量,直到条件信号被激活。

当一个线程在使用条件变量和互斥锁的时候,在条件不满足的时候(即条件信号为阻塞的时候),通过这个函数,该线程的锁会不断被解锁最后又被加锁,直到条件满足,被激活后此时会到重新上锁那一步,最后返回该函数,此时该函数执行语句结束,才会往下面的程序进行。

注意两点:

1) 在thread_cond_wait()之前,必须先lock相关联的mutex, 因为假如目标条件未满足,pthread_cond_wait()实际上会unlock该mutex, 然后block,在目标条件满足后再重新lock该mutex, 然后返回.–这点非常重要

2) 为什么是while(sum<100),而不是if(sum<100) ?这是因为在pthread_cond_signal()和pthread_cond_wait()返回之间,有时间差,假设在这个时间差内,还有另外一个线程t4又把sum减少到100以下了,那么t3在pthread_cond_wait()返回之后,显然应该再检查一遍sum的大小.这就是用while的用意

pthread_cond_wait() 用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或pthread_cond_broadcast来唤醒它。

pthread_cond_wait() 必须与pthread_mutex_lock() 配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread_cond_signal()或pthread_cond_broadcast,把该线程唤醒,使pthread_cond_wait()通过(返回)时,该线程又自动获得该mutex。

pthread_cond_signal()必须和pthread_mutex_unlock(lock_s) 配套使用,在发送信号或者广播之前,一定要先unlock mutex,因为阻塞在条件变量上的线程被唤醒的时候,要对mutex加锁,如果该线程被唤醒,而此时通知线程仍然锁住互斥锁,泽被唤醒线程立刻阻塞在互斥锁上。

异步信号

同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。

“同”字从字面上容易理解为一起动作

其实不是,“同”字应是指协同、协助、互相配合。

如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。

所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。例如Window API函数SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的LRESULT值返回给调用者。

在多线程​​编程​​里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

看到这里,很懵。异步信号怎么实现同步的,回到最初的起点,线程同步是什么,其实线程同步不是同时执行一段代码,而是协调协商来执行的,就是一个有序的过程,不让他们争抢资源,争抢一般都会有意外,很暴力……所以要有序进行,协商有次序进行。就和排队一个道理。

首先信号与任何线程都是异步的,它到达每一个线程的时间都不一样,那么多个线程接受异步信号的时候,只有一个线程首先获得该信号。此时如果并发的多个同样的信号被送到一个进程,该进程有许多线程,这些并发的同样的信号会被不同的线程获取,而由于是并发的,只是在一个时间段里,在这个时间段里还是有先后顺序的,此时所有线程屏蔽了这些信号,线程收到信号并被挂起然后激活处理的过程还是一个排队执行的并发过程,只是看似是同时发生的。

信号量

引用微博上看见的一个片段

“这段共享内存的使用是有竞争条件存在的,从文件锁的例子我们知道,进程间的通信绝不仅仅是通信那么简单,还需要在处理类似这样的临界区代码。在这里,我们也可以使用文件锁来处理,但是共享内存使用文件锁未免显得太不协调了。除了不方便以及效率低下以外,文件锁还不能够进行更高的进程控制。所以,我们在此需要更高级的进程同步控制原语来实现相关功能,这就是信号量的作用”

​https://www.2cto.com/kf/201708/665818.html​