场景设定
在终面的最后10分钟,面试官提出了一个高并发任务调度的优化问题,要求候选人使用asyncio
协程池来优化任务调度,同时兼顾任务优先级和资源限制。候选人需要快速实现代码,并解释协程池的工作原理及与传统线程池的差异。
面试流程
第一轮:问题陈述
面试官:小王,最后一个问题。我们有一个高并发任务调度系统,目前任务堆积严重。现在要求你使用asyncio
协程池来优化这个系统,同时需要支持任务优先级和资源限制。你需要在10分钟内完成代码实现,并解释协程池的工作原理以及它与传统线程池的差异。
候选人:好的,我理解了!我会用asyncio
的asyncio.Semaphore
限制并发数量,用优先级队列管理任务,再结合asyncio.create_task
来提交任务。我先用asyncio.Queue
来实现任务队列,然后用asyncio.Semaphore
来控制并发任务数。
第二轮:代码实现
候选人:(开始编码)
import asyncio
import heapq
from collections import deque
from functools import partial
# 任务优先级队列
class PriorityTaskQueue:
def __init__(self):
self.tasks = []
self.counter = 0 # 用于解决优先级相同时的比较问题
def put(self, task, priority=0):
heapq.heappush(self.tasks, (priority, self.counter, task))
self.counter += 1
def get(self):
return heapq.heappop(self.tasks)[-1]
def empty(self):
return len(self.tasks) == 0
# 协程任务调度器
class CoroutineTaskScheduler:
def __init__(self, max_concurrency=10):
self.task_queue = PriorityTaskQueue()
self.semaphore = asyncio.Semaphore(max_concurrency)
self.tasks = set()
async def submit_task(self, coroutine, priority=0):
# 将任务放入优先级队列
self.task_queue.put(coroutine, priority)
# 等待信号量许可
async with self.semaphore:
# 提交任务并跟踪
task = asyncio.create_task(coroutine)
self.tasks.add(task)
task.add_done_callback(partial(self._remove_task, task))
await task
def _remove_task(self, task, fut):
self.tasks.remove(task)
async def run(self):
while not self.task_queue.empty():
# 获取优先级最高的任务
task = self.task_queue.get()
await self.submit_task(task)
# 示例任务
async def example_task(name, delay):
print(f"Task {name} started.")
await asyncio.sleep(delay)
print(f"Task {name} completed.")
# 主程序
async def main():
scheduler = CoroutineTaskScheduler(max_concurrency=3)
# 提交任务
await scheduler.submit_task(example_task("A", 2), priority=1)
await scheduler.submit_task(example_task("B", 3), priority=2)
await scheduler.submit_task(example_task("C", 1), priority=0)
await scheduler.submit_task(example_task("D", 4), priority=1)
# 启动调度器
await scheduler.run()
# 运行主程序
asyncio.run(main())
第三轮:代码解释
候选人:我实现了两个主要部分:优先级队列和协程任务调度器。
-
优先级队列:
- 使用
heapq
实现了一个优先级队列,任务会被按照优先级(priority
)排序。 - 如果优先级相同,会通过
counter
字段来区分,确保排序的唯一性。
- 使用
-
协程任务调度器:
- 使用
asyncio.Semaphore
来限制并发任务数,确保不会同时运行过多的协程。 - 使用
asyncio.create_task
来提交协程任务,并通过add_done_callback
来追踪任务的完成状态。 run
方法会不断从优先级队列中取出任务,并通过信号量控制并发。
- 使用
-
示例任务:
- 我用了一个简单的
example_task
函数来模拟任务,任务会有不同的优先级和延迟时间。
- 我用了一个简单的
第四轮:协程池与线程池的差异
候选人:协程池和线程池的主要差异在于它们处理任务的方式不同:
-
线程池:
- 线程池使用真正的操作系统线程,每个线程都会占用独立的系统资源(如CPU和内存)。
- 线程之间的切换需要操作系统调度,开销较大。
- 线程池适合I/O密集型任务(如网络请求、文件读写),但也适合计算密集型任务。
-
协程池:
- 协程池基于
asyncio
事件循环,所有的任务都在一个线程中执行,通过协作式调度实现并发。 - 协程之间的切换由解释器控制,开销非常小,适合I/O密集型任务。
- 但由于是单线程模型,协程池不适合计算密集型任务,因为CPU密集的任务会阻塞事件循环。
- 协程池基于
-
任务调度:
- 线程池中的任务是并行执行的,多个线程可以同时运行。
- 协程池中的任务是并发执行的,虽然在一个线程中,但通过
await
可以让出控制权,模拟出并发效果。
-
资源消耗:
- 线程池会消耗更多的系统资源,尤其是线程数较多时。
- 协程池资源消耗极低,适合处理大量轻量级的I/O任务。
第五轮:面试官点评
面试官:你的实现思路很清晰,代码结构也不错。协程池的确适合I/O密集型任务,但需要注意不能用于CPU密集型任务,因为协程是单线程的,会被阻塞。你解释协程池与线程池的差异也很到位,说明你对异步编程的理解是深入的。
候选人:谢谢您的肯定!我也意识到协程池的局限性,如果任务中有CPU密集型操作,我会考虑结合线程池来解决。不过对于I/O密集型任务,协程池确实是一个高效的解决方案。
面试官:非常好,今天的面试就到这里了。我们会尽快给你答复,祝你一切顺利!
候选人:谢谢面试官!期待您的好消息!再见!
(面试官微笑着点头,结束面试)