.NET中如何锁定长耗时任务并向调用者正确反馈运行状态?
问题描述
需求:在处理Web请求的方法中,检测某后台长耗时任务是否正在运行;若未运行,则锁定该任务以阻止后续调用,启动任务并返回对应提示;若任务已运行,则返回任务正在执行的提示。
尝试的两种方案均存在问题:
方案一:跨线程解锁报错
此方案因跨线程调用Monitor.Exit违反.NET锁规则报错:
static object lockMethod = new object(); static object lockTask = new object(); public static string testLocksAndTasks() { //此方法执行速度较快,但为避免其他问题,我对整个方法加锁。这可能与本问题核心需求无关, //但由于我确实会在整个请求方法上加锁(因需整合日志及静态变量相关逻辑),故在此保留。 //该方法整体执行很快,真正耗时的代码在DoWorkInOtherThreadMethod()中,因此需在后台任务中运行。 lock (lockMethod) { if (Monitor.TryEnter(lockTask)) { Task.Run(DoWorkInOtherThreadMethod) .ContinueWith(ct=> Monitor.Exit(lockTask)); //糟糕!在独立任务中解锁不被允许! return "We locked lockTask and started the thread!"; } else return "Task called and executed by other request!"; } }
方案二:竞态条件+无法反馈真实状态
重构锁的位置后,存在竞态条件,且无法向原调用者反馈真实状态:
public static string testLocksAndTasks() { lock (lockMethod) { //检查是否已锁定! if (!Monitor.IsEntered(lockTask)) { Task.Run(()=> { //未锁定,尝试获取锁... if (Monitor.TryEnter(lockTask)) { DoWorkInOtherThreadMethod(); Monitor.Exit(lockTask); //注:我知道此处应包裹在TRY/CATCH/FINALLY中,只是简化示例代码。 } else { //糟了!这种情况实际可能发生,但我绝不想进入此分支!而且无法向testLocksAndTasks的初始调用者反馈该状态,因为我们处于新线程中! } }); return "We locked lockTask and started the thread!"; } else return "Task called and executed by other request!"; } }
正确实现方案
核心思路是在请求处理线程中完成锁的获取与状态判断,确保反馈准确;后台任务持有锁直到执行完毕。推荐使用SemaphoreSlim实现,它天然支持跨线程释放,适配场景需求:
// 初始计数1,限制同一时间仅一个任务执行 static readonly SemaphoreSlim taskSemaphore = new SemaphoreSlim(1, 1); static object lockMethod = new object(); public static string testLocksAndTasks() { lock (lockMethod) { // 尝试获取信号量,0毫秒超时表示立即返回结果 if (taskSemaphore.Wait(0)) { // 启动后台任务,用try/finally确保信号量必释放 _ = Task.Run(async () => { try { DoWorkInOtherThreadMethod(); } finally { taskSemaphore.Release(); } }); return "任务已启动,正在后台执行!"; } else { return "任务正在执行中,请稍后再试!"; } } }
方案说明
- SemaphoreSlim适配场景:它允许在获取线程之外释放锁,完美解决
Monitor跨线程解锁的限制,无需额外逻辑处理线程归属问题。 - 状态判断原子性:在
lock(lockMethod)的保护下,taskSemaphore.Wait(0)的结果是原子性的,不会出现竞态条件——要么成功获取锁并启动任务,要么直接返回任务运行中提示,反馈完全准确。 - 异常安全:
try/finally块确保无论任务成功或抛出异常,信号量都会被释放,避免锁永久占用导致后续请求无法执行任务。
如果坚持使用Monitor,也可以通过静态标志位配合锁实现:
static object lockMethod = new object(); static object taskLock = new object(); static bool isTaskRunning = false; public static string testLocksAndTasks() { lock (lockMethod) { if (!isTaskRunning) { isTaskRunning = true; _ = Task.Run(() => { lock (taskLock) { try { DoWorkInOtherThreadMethod(); } finally { // 任务结束后重置状态标志 isTaskRunning = false; } } }); return "任务已启动,正在后台执行!"; } else { return "任务正在执行中,请稍后再试!"; } } }
此方案说明
- 用
isTaskRunning静态标志位记录任务状态,在lock(lockMethod)保护下修改和读取,确保状态判断的原子性。 - 后台任务中
lock(taskLock)作为双重保险,避免极端情况下多个任务同时执行。 finally块重置标志位,确保后续请求能正常触发任务。
内容的提问来源于stack exchange,提问作者RLH
相关产品推荐
相关产品推荐

