一个workqueue导致的性能问题

本文详细分析了一个由于workqueue导致的Linux系统性能下降问题。问题出现在work1和work2都在队列上,work1的长时间执行阻碍了work2的及时处理。通过深入理解workqueue的工作原理,发现在工作线程忙碌时,不会唤醒新的worker处理后续work,这在CPU密集型work中可能导致性能瓶颈。解决方案是使用WQ_CPU_INTENSIVE标志创建workqueue,确保CPU密集型work在独立线程中执行,从而修复了性能问题。

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

本文代码基于linux 4.19.195
笔者最近遇到了一个workqueue导致性能问题,引发了笔者对workqueue机制的探索和思考。
简单的抽象后,问题是这样的:
一共有两个进程,假设称之为a进程和b进程。a进程在等待b进程完成一些工作,b进程在完成工作后会调用相关接口通知a进程。b进程完成工作的流程的最末尾的位置,有如下两个关键步骤:

  1. 首先调用schedule_work_on触发一个work 1,b进程只需要触发即可,无需等待work 1完成;
  2. 然后再调用work_on_cpu(本质上也是schedule_work_on)完成触发一个work 2,然后等待work 2完成后再返回。

在之前的代码里,是没有步骤1的,性能是正常的,耗时约5s。
在近期的开发里,把步骤1加入后,发现性能下降了3倍,耗时15s。

触发了该问题后,先抓了正常代码和异常代码的on-cpu火焰图,发现火焰图长的几乎一模一样。看来,并不是代码执行的时候有性能问题。
其次,check了一下work 1的代码,发现work 1的代码是cpu消耗型,但是在循环里有cond_resched(),也就是说,按理不会一直占着cpu不放,即使是和其他进程抢着cpu使用,那也不至于多花10s的时间。
思考了一下,会不会是这样呢:work 1和work 2都在队列里排队,然后work1先执行,但是一直没有返回,然后导致了work 2一直得不到执行,从而造成了性能的下降。
那么,抓了一下top的输出,发现,每次a进程成功返回的时候,基本和work 1退出的时间吻合。
但是,想了想,还是说不通,因为,workqueue有着自己的管理机制,在任务多的时候,是会通过创建多个kworker去执行的。

static int worker_thread(void *__worker)
{
	***
	/* do we need to manage? */
	if (unlikely(!may_start_working(pool)) && manage_workers(worker)) //may_start_working()判断pool中是否有idle状态工作线程。如果没有,那么manage_workers()创建一些工作线程。
		goto recheck;

	/*
	 * ->scheduled list can only be filled while a worker is
	 * preparing to process a work or actually processing it.
	 * Make sure nobody diddled with it while I was sleeping.
	 */
	WARN_ON_ONCE(!list_empty(&worker->scheduled)); 
	/*
	 * Finish PREP stage.  We're guaranteed to have at least one idle
	 * worker or that someone else has already assumed the manager
	 * role.  This is where @worker starts participating in concurrency
	 * management if applicable and concurrency management is restored
	 * after being rebound.  See rebind_workers() for details.
	 */
	worker_clr_flags(worker, WORKER_PREP | WORKER_REBOUND);

	do {
		struct work_struct *work =
			list_first_entry(&pool->worklist,
					 struct work_struct, entry);

		pool->watchdog_ts = jiffies;

		if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
			/* optimization path, not strictly necessary */
			process_one_work(worker, work);
			if (unlikely(!list_empty(&worker->scheduled)))
				process_scheduled_works(worker);
		} else { 
			move_linked_works(work, &worker->scheduled, NULL);
			process_scheduled_works(worker);
		}
	} while (keep_working(pool));

	***
}

may_start_working()会判断pool中是否有idle状态工作线程。如果没有,那么会调用manage_workers()创建一些工作线程,在manage_workers()函数中会把这些kworker线程状态设置成idle。也就是说,workqueue这个系统,除了正在执行的kworker线程,至少会在该work_pool保留一个idle的kworker线程。当然,workqueue模块也不会放任idle的kworker数量不断增长,这个机制可以查看idle_worker_timeout函数,不过,根据too_many_workers()函数的逻辑,感觉一般情况下至少会保留2个idle的kworker

/**
 * manage_workers - manage worker pool
 * @worker: self
 *
 * Assume the manager role and manage the worker pool @worker belongs
 * to.  At any given time, there can be only zero or one manager per
 * pool.  The exclusion is handled automatically by this function.
 *
 * The caller can safely start processing works on false return.  On
 * true return, it's guaranteed that need_to_create_worker() is false
 * and may_start_working() is true.
 *
 * CONTEXT:
 * spin_lock_irq(pool->lock) which may be released and regrabbed
 * multiple times.  Does GFP_KERNEL allocations.
 *
 * Return:
 * %false if the pool doesn't need management and the caller can safely
 * start processing works, %true if management function was performed and
 * the conditions that the caller verified before calling the function may
 * no longer be true.
 */
static bool manage_workers(struct worker *worker)
{
	struct worker_pool *pool = worker->pool;

	if (pool->flags & POOL_MANAGER_ACTIVE)
		return false;

	pool->flags |= POOL_MANAGER_ACTIVE;
	pool->manager = worker;

	maybe_create_worker(pool);

	pool->manager = NULL;
	pool->flags &= ~POOL_MANAGER_ACTIVE;
	wake_up(&wq_manager_wait);
	return true;
}

理论上走不通了,那就继续复现抓线索。发现,在a进程等待的第5秒到第15秒中,一直有一个进程处于D状态,而且,这个进程就是b进程,并且,调用栈就是在等待work 2的完成。
看来,上面分析的没有错,确实就是work 1和work 2都在队列上,然后work 1一直没执行完,从而导致的性能下降。
看来,我对workqueue的机制还是没有理解透彻,继续翻看代码。
我们插入work的时候,调用的是schedule_work_on()函数,这个函数最终会调用到insert_work()函数

/**
 * insert_work - insert a work into a pool
 * @pwq: pwq @work belongs to
 * @work: work to insert
 * @head: insertion point
 * @extra_flags: extra WORK_STRUCT_* flags to set
 *
 * Insert @work which belongs to @pwq after @head.  @extra_flags is or'd to
 * work_struct flags.
 *
 * CONTEXT:
 * spin_lock_irq(pool->lock).
 */
static void insert_work(struct pool_workqueue *pwq, struct work_struct *work,
			struct list_head *head, unsigned int extra_flags)
{
	struct worker_pool *pool = pwq->pool;

	/* we own @work, set data and link */
	set_work_pwq(work, pwq, extra_flags);
	list_add_tail(&work->entry, head);
	get_pwq(pwq);

	/*
	 * Ensure either wq_worker_sleeping() sees the above
	 * list_add_tail() or we see zero nr_running to avoid workers lying
	 * around lazily while there are works to be processed.
	 */
	smp_mb();

	if (__need_more_worker(pool))
		wake_up_worker(pool);
}

注意最后两行代码,从字面上来看,插入这个work到队列里面后,先通过函数__need_more_worker(pool)判断是否需要一个新的kworker来处理他,如果需要,则调用wake_up_worker(pool)函数完成kworker的唤醒操作。
显然,在这个问题中,__need_more_worker(pool)函数返回的值是false。

/*
 * Policy functions.  These define the policies on how the global worker
 * pools are managed.  Unless noted otherwise, these functions assume that
 * they're being called with pool->lock held.
 */

static bool __need_more_worker(struct worker_pool *pool)
{
	return !atomic_read(&pool->nr_running);
}

看来,如果这个worker_pool里有kworker在跑,在insert_work()时就不会唤醒idle的kworker来处理下一个在排队的work。这样的策略,在这个问题的场景下,就会导致性能问题。这个策略,其实,也不是说不合理,毕竟,有些work只是不适合在中断里处理,从而才放到workqueue里面来做,并不是说每个work都会处理很长的时间。从而,这样的策略,能够减少kworker被唤醒的次数,从而可以为系统减压,当然,带来的结果就是潜在的效率的下降。感觉,这个策略是一个折中的策略,毕竟,如果唤醒一个kworker,起来做了那么多准备,最后只是执行几行代码这个work就退出了的话,确实也太浪费cpu资源了。
好吧,到此为止至少问题是分析清除了,那么该如何解决呢?
schedule_work_on函数使用的是system_wq这个workqueue,那我们自己调用create_workqueue()创建一个新的workqueue能不能解决呢?
答案是不行。因为基于workqueue的设计理念(这里就不讲了,请自行百度),create_workqueue()创建一个新的workqueue最后大概率也是会和system_wq用同一个work_pool,这样的话问题依然存在。
看了下代码,发现用WQ_CPU_INTENSIVE这个flag创建一个workqueue是最合适的,这个flag创建的workqueue,其work会在单独一个kworker上执行,而不会和其他work使用同一个kworker线程,详情见process_one_work函数中的cpu_intensive临时变量。
嗯,改代码,验证,性能问题成功fix。

趁周末时间,再次了解了一下workqueue调度的思想:

如果有 work 需要处理,保持一个 running 状态的 worker 处理,不多也不少。
但是这里有一个问题如果 work 是 CPU 密集型的,它虽然也没有进入 suspend 状态,但是会长时间的占用 CPU,让后续的 work 阻塞太长时间。
为了解决这个问题,CMWQ 设计了 WQ_CPU_INTENSIVE,如果一个 wq 声明自己是 CPU_INTENSIVE,则让当前 worker 脱离动态调度,像是进入了 suspend 状态,那么 CMWQ 会创建新的 worker,后续的 work 会得到执行

看来,workqueue确实不愿意使用多个kworker线程处理work,而是依靠一个kworker一个个的把work处理完。

<think>嗯,用户想了解如何创建一个线程池。首先,我需要回忆线程池的基本概念和作用。线程池主要用来管理多个线程,避免频繁创建和销毁线程带来的开销,提高系统性能。常见的线程池参数包括核心线程数、最大线程数、存活时间、工作队列和拒绝策略等。 接下来,我需要确定用户可能使用的编程语言。虽然问题里没有明确说明,但常见的如Java、Python等都有线程池的实现。考虑到Java中的ThreadPoolExecutor是比较典型的例子,可能更适合作为示例。不过Python的concurrent.futures模块也不错,更简洁。但用户可能需要更底层的配置,所以Java可能更合适。 然后,要分步骤解释创建线程池的过程。首先,核心参数有哪些,每个参数的意义。比如核心线程数、最大线程数、存活时间、工作队列类型、拒绝策略。然后给出具体代码示例,比如Java中如何使用ThreadPoolExecutor类,或者Executors工厂类创建不同类型的线程池,比如FixedThreadPool、CachedThreadPool等。 同时,需要提醒注意事项,比如合理配置线程数量,避免资源耗尽;处理异常;正确关闭线程池等。可能还要提到不同工作队列的选择,比如有界队列和无界队列的区别,以及拒绝策略的选择,比如直接丢弃、调用者运行等。 还要考虑用户可能的实际应用场景,比如Web服务器处理请求,批量处理任务等,说明线程池如何优化这些场景的性能。此外,是否需要对比手动创建线程和使用线程池的优缺点,强调线程池的优势。 最后,要确保回答结构清晰,分步骤说明,每个部分有代码示例和解释。检查是否符合系统级指令,比如数学公式是否正确使用$...$,但这个问题可能不涉及数学公式,所以可能不需要。但如果有代码块,需要用正确的标记,比如Java代码用三个反引号包围,并注明语言。不过用户给的示例里没有代码块,可能需要注意这一点。或者根据用户的要求,可能只需要文字描述和关键代码片段。 总结一下,回答的步骤应该是:1. 线程池的作用;2. 关键参数解释;3. 创建步骤和示例代码;4. 注意事项和最佳实践。确保用中文,结构清晰,分点说明,示例正确。</think>创建线程池需要根据具体编程语言和框架进行设计,以下是通用实现思路和关键步骤(以Java为例): 一、线程池核心参数 1. 核心线程数 $corePoolSize$:保留的最小工作线程数量 2. 最大线程数 $maximumPoolSize$:允许创建的最大线程数量 3. 存活时间 $keepAliveTime$:非核心线程的空闲存活时间(时间单位需指定) 4. 工作队列 $workQueue$:存储待执行任务的阻塞队列 5. 线程工厂 $threadFactory$:创建新线程的工厂 6. 拒绝策略 $rejectedExecutionHandler$:队列满时的处理策略 二、Java实现示例 ```java ThreadPoolExecutor executor = new ThreadPoolExecutor( 5, // corePoolSize 10, // maximumPoolSize 60L, // keepAliveTime TimeUnit.SECONDS, // 时间单位 new LinkedBlockingQueue<>(100), // 工作队列 Executors.defaultThreadFactory(), // 线程工厂 new ThreadPoolExecutor.AbortPolicy() // 拒绝策略 ); ``` 三、标准创建步骤 1. 确定任务类型:CPU密集型(建议线程数 = CPU核数+1)或I/O密集型(建议线程数 = 2*CPU核数) 2. 配置核心参数: - 核心线程数 ≈ 平均并发需求 - 最大线程数 = 核心线程数 + 突发流量余量 - 队列容量根据内存和响应时间要求设定 3. 选择队列类型: - 有界队列(如ArrayBlockingQueue):避免资源耗尽 - 无界队列(如LinkedBlockingQueue):可能引起内存溢出 4. 设置拒绝策略: - AbortPolicy(默认):抛出RejectedExecutionException - CallerRunsPolicy:由提交任务的线程执行 - DiscardPolicy:静默丢弃任务 - DiscardOldestPolicy:丢弃队列最旧任务 四、最佳实践建议 1. 监控线程池状态:定期检查活跃线程数/队列大小 2. 使用线程工厂命名线程:便于问题排查 3. 优雅关闭: ```java executor.shutdown(); // 停止接收新任务 if(!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); // 强制终止 } ``` 4. 异常处理:重写afterExecute()方法捕获任务异常 五、常见线程池类型 1. FixedThreadPool(固定大小):Executors.newFixedThreadPool(n) 2. CachedThreadPool(弹性扩容):Executors.newCachedThreadPool() 3. ScheduledThreadPool(定时任务):Executors.newScheduledThreadPool(n) 4. WorkStealingPool(并行处理):Executors.newWorkStealingPool() 注意:实际开发中建议直接使用ThreadPoolExecutor构造方法,而非Executors工厂方法,以避免隐藏不合理的默认参数设置(如Integer.MAX_VALUE线程数导致资源耗尽风险)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值