Prometheus Gauge抛出LockRecursionException:是库Bug还是用法错误?
问题:跟踪队列长度时出现LockRecursionException异常
我编写了一段用于跟踪队列长度的代码:
private static Gauge enqueuedWorkGauge = Metrics .CreateGauge(...); public async Task ProcessMessageAsync(Task asyncWork) { // increment counter because something is about to be added to the queue enqueuedWorkGauge .WithLabels(label1, label2) .Inc(); // enqueue work; enqueueing might need to wait, so it is async var workCompletedTask = await queue.EnqueueWorkAsync(asyncWork); return workCompletedTask .ContinueWith( t => { // work is done! // element is not in the queue anymore enqueuedWorkGauge .WithLabels(label1, label2) .Dec(); }, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default ); }
有时会在日志中观测到如下异常:
System.Threading.LockRecursionException: Recursive read lock acquisitions not allowed in this mode. at System.Threading.ReaderWriterLockSlim.TryEnterReadLockCore(TimeoutTracker timeout) at Prometheus.Collector`1.TryGetLabelled(LabelSequence instanceLabels, TChild& child) at ProcessMessageAsync
该异常抛出自prometheus-net库内部的Collector类相关逻辑,请问这是prometheus-net库的Bug,还是我对任务的使用方式有误?
原因分析与解决方案
这不是prometheus-net的Bug,问题出在你对任务延续的使用方式上:
- 你指定了
*TaskContinuationOptions.ExecuteSynchronously*,这会让延续任务在完成workCompletedTask的线程上同步执行。 - 当
workCompletedTask恰好是在调用await queue.EnqueueWorkAsync的同一个线程上完成时,当前线程可能还持有prometheus-net内部*ReaderWriterLockSlim*的读锁(之前调用Inc()时,*WithLabels*方法会获取读锁),此时再次调用*WithLabels*尝试获取读锁,就会触发递归读锁的异常(因为*ReaderWriterLockSlim*默认不允许递归获取读锁)。
修复方案
推荐使用更简洁安全的await替代ContinueWith,同时用finally确保计数器无论任务成功失败都会递减:
private static Gauge enqueuedWorkGauge = Metrics .CreateGauge(...); public async Task ProcessMessageAsync(Task asyncWork) { enqueuedWorkGauge.WithLabels(label1, label2).Inc(); var workCompletedTask = await queue.EnqueueWorkAsync(asyncWork); try { await workCompletedTask; } finally { enqueuedWorkGauge.WithLabels(label1, label2).Dec(); } }
如果坚持使用ContinueWith,则需要移除TaskContinuationOptions.ExecuteSynchronously选项,让延续任务在ThreadPool线程上执行,避免递归锁问题:
return workCompletedTask .ContinueWith( t => { enqueuedWorkGauge.WithLabels(label1, label2).Dec(); }, default, // 移除ExecuteSynchronously选项 TaskScheduler.Default );
这两种方式都能避免递归锁的问题,其中await版本的代码可读性和可维护性更优。
内容的提问来源于stack exchange,提问作者Garfield
相关产品推荐
相关产品推荐

