线程池设计中的惊群问题

http://www.myexception.cn/open-source/1587656.html

多线程编程已经是现在网络编程中常用的编程技术,设计一个良好的线程池库显得尤为重要。在 UNIX(WIN32下可以采用类似的方法,acl 库中的线程池是跨平台的) 环境下设计线程池库主要是如何用好如下系统 API:

      1、pthread_cond_signal/pthread_cond_broadcast:生产者线程通知线程池中的某个或一些消费者线程池,接收处理任务;

      2、pthread_cond_wait:线程池中的消费者线程等待线程条件变量被通知;

      3、pthread_mutex_lock/pthread_mutex_unlock:线程互斥锁的加锁及解锁函数。

 

      下面的代码示例是大家常见的线程池的设计方式:

 

// 线程任务类型定义
struct thread_job {
	struct thread_job *next;  // 指向下一个线程任务
	void (*func)(void*);      // 应用回调处理函数 
	void *arg;                // 回调函数的参数
	...
};

// 线程池类型定义
struct thread_pool {
	int   max_threads;        // 线程池中最大线程数限制
	int   curr_threads;       // 当前线程池中总的线程数
	int   idle_threads;       // 当前线程池中空闲的线程数
	pthread_mutex_t mutex;    // 线程互斥锁
	pthread_cond_t  cond;     // 线程条件变量
	thread_job *first;        // 线程任务链表的表头
	thread_job *last;         // 线程任务链表的表尾
	...	
}

// 线程池中的消费者线程处理过程
static void *consumer_thread(void *arg)
{
	struct thread_pool *pool = (struct thread_pool*) arg;
	struct thread_job  *job;
	int   status;

	// 该消费者线程需要先加锁
	pthread_mutex_lock(&pool->mutex);

	while (1) {
		if (pool->first != NULL) {
			// 有线程任务时,则取出并在下面进行处理
			job = pool->first;
			pool->first = job->next;
			if (pool->last == job)
				pool->last = NULL;

			// 解锁,允许其它消费者线程加锁或生产者线程添加新的任务
			pthread_mutex_unlock(&pool->mutex);

			// 回调应用的处理函数
			job->func(job->arg);

			// 释放动态分配的内存
			free(job);

			// 重新去加锁
			pthread_mutex_lock(&pool->mutex);
		} else {
			pool->idle_threads++;

			// 在调用 pthread_cond_wait 等待线程条件变量被通知且自动解锁
			status = pthread_cond_wait(&pool->cond, &pool->mutex);

			pool->idle_threads--;

			if (status == 0)
				continue;

			// 等待线程条件变量异常,则该线程需要退出
			pool->curr_threads--;
			pthread_mutex_unlock(&pool->mutex);
			break;
		}
	}

	return NULL;
}

// 生产者线程调用此函数添加新的处理任务
void add_thread_job(struct thread_pool *pool, void (*func)(void*), void *arg)
{
	// 动态分配任务对象
	struct thread_job *job = (struct thread_job*) calloc(1, sizeof(*job));

	job->func = func;
	job->arg = arg;

	pthread_mutex_lock(&pool->mutex);

	// 将新任务添加进线程池的任务链表中
	if (pool->first == NULL)
		pool->first = job;
	else
		pool->last->next = job;
	pool->last = job;
	job->next = NULL;
	
	if (pool->idle_threads > 0) {
		// 如果有空闲消费者线程,则通知空闲线程进行处理,同时需要解锁

		pthread_mutex_unlock(&pool->mutex);
		pthread_cond_signal(&pool->cond);
	} else if (pool->curr_threads < pool->max_threads) {
		// 如果未超过最大线程数限制,则创建一个新的消费者线程

		pthread_t id;
		pthread_attr_t attr;

		pthread_attr_init(&attr);

		// 将线程属性设为分享模式,这样当线程退出时其资源自动由系统回收
		pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

		// 创建一个消费者线程
		if (pthread_create(&id, &attr, consumer_thread, pool) == 0)
			pool->curr_threads++;

		pthread_mutex_unlock(&pool->mutex);
		pthread_attr_destroy(&attr);
	}
}

// 创建线程池对象
struct thread_pool *create_thread_pool(int max_threads)
{
	struct thread_pool *pool = (struct thread_pool*) calloc(1, sizeof(*pool));
	
	pool->max_threads = max_threads;
	...

	return pool;
}

///
// 使用上面线程池的示例如下:

// 由消费者线程回调的处理过程
static void thread_callback(void* arg)
{
      ...
}

void test(void)
{
	struct thread_pool *pool = create_thread_pool(100);
	int   i;

	// 循环添加 1000000 次线程处理任务
	for (i = 0; i < 1000000; i++)
		add_thread_job(pool, thread_callback, NULL);
}

 

      乍一看去,似乎也没有什么问题,象很多经典的开源代码中也是这样设计的,但有一个重要问题被忽视了:线程池设计中的惊群现象。大家可以看到,整个线程池只有一个线程条件变量和线程互斥锁,生产者线程和消费者线程(即线程池中的子线程)正是通过这两个变量进行同步的。生产者线程每添加一个新任务,都会调用 pthread_cond_signal 一次,由操作系统唤醒一个在线程条件变量等待的消费者线程,但如果查看 pthread_cond_signal API 的系统帮助,你会发现其中有一句话:调用此函数后,系统会唤醒在相同条件变量上等待的一个或多个线程。而正是这句模棱两可的话没有引起很多线程池设计者的注意,这也是整个线程池中消费者线程收到信号通知后产生惊群现象的根源所在,并且是消费者线程数量越多,惊群现象越严重----意味着 CPU 占用越高,线程池的调度性能越低。

      要想避免如上线程池设计中的惊群问题,在仍然共用一个线程互斥锁的条件下,给每一个消费者线程创建一个线程条件变量,生产者线程在添加任务时,找到空闲的消费者线程,将任务置入该消费者的任务队列中,同时只通知 (pthread_cond_signal) 该消费者的线程条件变量,消费者线程与生产者线程虽然共用相同的线程互斥锁(因为有全局资源及调用 pthread_cond_wait 所需,并且linux互斥锁每次只会唤醒一个线程),但线程条件变量的通知过程却是定向通知的,未被通知的消费者线程不会被唤醒,这样惊群现象也就不会产生了。

      当然,还有一些设计上的细节需要注意,比如:当没有空闲消费者线程时,需要将任务添加进线程池的全局任务队列中,消费者线程处理完自己的任务后需要查看一下线程池中的全局任务队列中是否还有未处理的任务。

      更多的线程池的设计细节请参考 acl (https://sourceforge.net/projects/acl/) 库中 lib_acl/src/thread/acl_pthread_pool.c 中的代码。

 

 参考:

线程编程常见API简介(上)

线程编程常见API简介(中)

线程编程常见API简介(下)

使用 acl_cpp 库编写多线程程序

利用ACL库开发高并发半驻留式线程池程序

多线程开发时线程局部变量的使用

再谈线程局部变量

 

acl 库下载:https://sourceforge.net/projects/acl/

github:https://github.com/zhengshuxin/acl

svn:svn checkout svn://svn.code.sf.net/p/acl/code/trunk acl-code

qq 群:242722074
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python的ThreadPoolExecutor是线程池的一种实现方式,它提供了方便的接口来进行并发编程。在使用ThreadPoolExecutor时,通常会遇到异常捕获的问题。 当线程池线程执行任务时,如果任务发生异常,异常会被捕获,并通过Future对象返回给调用者。我们可以通过检查Future对象的状态来获取异常信息。Future对象是一个表示异步计算结果的对象,它可以用来检查任务是否完成、取消任务、获取任务的结果等。 在ThreadPoolExecutor,可以通过submit方法来提交任务。这个方法返回一个Future对象,我们可以通过调用Future对象的result方法来等待任务完成并获取任务的结果。如果任务发生异常,result方法将会抛出异常,并将异常的类型和信息传递给调用者。 另外,我们还可以通过调用ThreadPoolExecutor的shutdown方法来关闭线程池。关闭线程池后,任何待处理的任务将会被取消,并且已提交但还未开始执行的任务将会被清除。我们可以通过调用Future对象的cancel方法来取消任务。 在代码,我们可以使用try-except语句块来捕获线程任务的异常。可以使用ThreadPoolExecutor的submit方法来提交任务,并通过返回的Future对象来获取任务的结果。在调用Future对象的result方法时,如果发生了异常,可以使用try-except语句块来捕获异常并处理异常。另外,在使用完线程池后,我们应该调用shutdown方法来关闭线程池,以释放资源。 总结起来,Python的ThreadPoolExecutor提供了异常捕获机制,我们可以通过检查返回的Future对象来获取任务执行过程的异常信息。在使用完线程池后,我们应该及时关闭线程池,以释放资源。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值