Quartz JobWasExecuted中调用TaskCompletionSource.Task致程序挂起排查
在Quartz调用的JobWasExecuted()方法内,等待返回TaskCompletionSource.Task的方法时,应用程序出现无响应("静默")情况。
场景
我使用Quartz调度任务,为了在任务执行后收到通知以执行后续操作,让一个类型继承JobListenerSupport并重写Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default)方法。
后续操作包含调用其他类型的方法,这些方法通过TaskCompletionSource将基于事件的模式转换为基于任务的模式,示例代码如下:
public Task<List<ScanResult>> Subscribe(ScanRequest scanRequest) { var requestId = CreateRequestId(); var taskCompletionSource = new TaskCompletionSource<List<ScanResult>>(); scanRequestResultsByReqId.TryAdd(requestId, new ScanRequestResult(scanRequest, taskCompletionSource)); ClientSocket.reqScannerSubscription( requestId, scanRequest.ScannerSubscription, scannerSubscriptionOptions: null, scanRequest.ScannerSubscriptionOptions); return taskCompletionSource.Task; }
这些方法最终都会从JobWasExecuted()中被调用,示例代码如下:
public override async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default) { await Do(); // ... await base.JobWasExecuted(context, jobException, cancellationToken); } public async Task Do() { var scanResults = await scanner.Subscribe(CreateScanRequest()); // ... }
当在JobWasExecuted()方法内的任意位置等待返回TaskCompletionSource.Task的方法(如Do()中的调用)时,应用程序就会无响应。
若绕过Quartz和JobWasExecuted()方法,直接从应用入口调用Do(),整个应用运行正常,这表明问题可能与线程机制相关,但我尚未理清其中缘由。
更新补充
- 我未使用任何
.Result、.Wait或类似同步阻塞方法。 - 当调试暂停无响应的应用时,总能定位到第三方库中的如下循环(该库是我消费事件的来源):
while (eClientSocket.IsConnected()) { if (!eClientSocket.IsDataAvailable()) { Thread.Sleep(1); continue; } if (!putMessageToQueue()) break; }
使用传统回调方式时一切正常,切换为TaskCompletionSource后才出现该问题,且仅在Quartz的JobWasExecuted()回调中触发。
请问我忽略了什么关键点?
1. Quartz线程池的同步上下文绑定问题
Quartz的JobWasExecuted运行在其专用工作线程池内,默认情况下await会捕获当前的同步上下文(SynchronizationContext)。如果Quartz工作线程的同步上下文与第三方库的消息处理逻辑冲突,就会形成隐性死锁:
- 第三方库的
while循环一直在占用Quartz工作线程轮询数据 await后的代码需要等待该线程释放才能继续执行,但线程被轮询循环阻塞,无法触发TaskCompletionSource.SetResult()完成等待
而直接从应用入口调用时,线程不受Quartz线程池限制,同步上下文不同,第三方库的消息循环能正常触发任务完成,不会阻塞。
2. 解决方案建议
- 解除同步上下文绑定:在所有
await调用后添加ConfigureAwait(false),避免捕获Quartz的同步上下文,让后续代码在ThreadPool线程执行,不占用Quartz工作线程:public override async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default) { await Do().ConfigureAwait(false); // ... await base.JobWasExecuted(context, jobException, cancellationToken).ConfigureAwait(false); } public async Task Do() { var scanResults = await scanner.Subscribe(CreateScanRequest()).ConfigureAwait(false); // ... } - 隔离第三方库调用线程:将
Subscribe的调用转移到独立ThreadPool线程,避免Quartz工作线程被第三方库的轮询循环占用:public async Task Do() { var scanResults = await Task.Run(() => scanner.Subscribe(CreateScanRequest())).ConfigureAwait(false); // ... } - 确保
TaskCompletionSource线程安全:如果第三方库的事件回调在非调用线程执行,确认SetResult()的调用是线程安全的,必要时使用TaskCompletionSource.TrySetResult()避免重复触发异常。
内容的提问来源于stack exchange,提问作者user1323245

