线程池死锁问题
我的 OJ 架构不算复杂:用户提交代码 -> 消息队列解耦 ->
Judge模块接收消息 -> 分发给线程池执行。
为了提高效率,我设计了二级异步:
- 调度层:单线程从队列获取任务。
- 执行层:丢进
JudgingThreadPool 异步执行。
- 任务内部:编译代码、跑多个测试用例
为了快,这些子任务也用了多线程。
初始的AI给我的代码 省略一些细节处理 只保留了核心
(并且除了死锁外,AI的代码还没有处理compile过慢的情况,会导致错误识别成编译错误)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| private void pollAndDispatch() { while (running.get()) { try { JudgingTask task = poll(JUDGING_QUEUE_KEY, POLL_TIMEOUT_SECONDS, TimeUnit.SECONDS); if (task != null) { judgingExecutor.execute(() -> executeTask(task)); } } catch (Exception e) { } } }
private void executeTask(JudgingTask task) { try { CompletableFuture<CompileResult> compileFuture = CompletableFuture.supplyAsync( () -> sandboxService.compile(task.getCodePath(), task.getLang()), judgingExecutor );
List<TestCase> testCases = loadTestCases(task.getProblemId()); CountDownLatch latch = new CountDownLatch(testCases.size()); for (TestCase tc : testCases) { CompletableFuture.supplyAsync( () -> sandboxService.runSingleTestCase(task, tc), judgingExecutor ).whenComplete((res, ex) -> latch.countDown()); }
latch.await(60, TimeUnit.SECONDS); JudgeResult finalResult = aggregateResults(compileFuture.get(), runFutures); updateSubmissionStatus(task.getSubmissionId(), finalResult); } catch (Exception e) { } }
|
然后进行压测的时候,同时执行了100次提交,结果一直在WAIT
查看了线程池信息发现
1 2 3
| ThreadPool Size: [64] Active Threads: 64 Number of Tasks in Queue: 377
|
线程池里的线程都消耗完了 并且队列里有大量阻塞的任务 CPU却不转
很可能就是死锁 导致所有线程park了
用 jstack 导出了线程栈,发现线程全部卡在
CountDownLatch.await()
原因很简单:
- 有一部分executeTask先抢到了线程池里的线程
- 这些父任务又提交了运行测试用例的子任务到同一个线程池,但在提交过程中线程池爆了,导致子任务一直在队列里
- 这样就出现了死锁:子任务只能在队列里排队,而所有的父任务在等所有子任务运行完才能释放线程。
解决方法
将父子任务的线程池分离
这样即使子任务堆积也不会影响到父任务对应的线程池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| private void executeTask(JudgingTask task) { try { CompileResult compileResult = CompletableFuture.supplyAsync( () -> sandboxService.compile(task.getCodePath(), task.getLang()), sandboxExecutor ).get(10, TimeUnit.SECONDS);
if (!compileResult.isSuccess()) { updateSubmissionStatus(task.getSubmissionId(), JudgeResult.compileError()); return; }
List<TestCase> testCases = loadTestCases(task.getProblemId()); List<CompletableFuture<RunResult>> futures = testCases.stream() .map(tc -> CompletableFuture.supplyAsync( () -> sandboxService.runSingleTestCase(task, tc), sandboxExecutor )) .toList();
CompletableFuture.allOf(runFutures.toArray(new CompletableFuture[0])) .exceptionally(ex -> { throw new CompletionException(ex); }) .join();
JudgeResult finalResult = aggregateResults(compileResult, results); updateSubmissionStatus(task.getSubmissionId(), finalResult);
} catch (Exception e) { } }
|
经验总结
- 线程池最好不要始终只用一个 尤其是任务之间有父子关系的情况
父任务提交子任务时很容易出现死锁;
并且出问题后也不好定位到底是哪个任务导致的死锁。
- 线程池中谨慎使用wait和join这些操作
其实上面如果加个超时等待还能好一点,避免把队列撑爆 OOM
最后对 KIMI-K2.5 有点失望啊 除了上面这个问题
下面启动一个前端项目跑了20分钟,就光启动这个项目没有其他操作;
因为我的项目是前后端一块的,前端放在src/目录下,npm run dev要cd
src里进行;
但kimi-2.5一直察觉不到,并且有时候判断出不在目录下后,过了几条命令又开始抽风了
alt text