Skip to main content
 首页 » 编程设计

c#之对队列任务使用锁是一种好习惯吗

2024年02月04日32dflying

我需要将一些可以随时到达的请求放入队列中,这样每个任务仅在前一个任务结束时才开始。问题是,为此目的使用锁定是个好主意吗?它是否有任何不良影响,是否会导致我预期的排队行为?

更具体地说,考虑代码:

private int MyTask() { 
   ... 
} 
 
private object someLock = new object(); 
 
public Task<int> DoMyTask() { 
    return Task.Run(() => 
    { 
        lock (someLock) 
        { 
            return MyTask(); 
        } 
    }); 
} 
 
public void CallMyTask() { 
    var result = await DoMyTask(); 
} 

请注意,CallMyTask() 将随时调用,可能同时调用。

请您参考如下方法:

is it a good idea to use locking for this purpose? Does it have any bad effects, and do the queuing behavior that I expect result from this?

锁定在这里不是一个好的解决方案。不良影响是它会阻塞一个线程池线程从工作在队列中直到工作完成。因此,如果您的代码排队 1000 个请求,它将调用 Task.Run 1000 次,并可能用完该数量的线程池线程,每个线程都在等待锁。

此外,锁并不是严格意义上的 FIFO。他们只是大多数-sorta-FIFO。这是因为严格 FIFO 锁会导致其他问题,例如锁车队; the links in this issue have some great discussion about lock "fairness" (i.e., FIFO behavior) .

所以,我推荐一个实际的队列。您可以使用 ActionBlock<T>来自 TPL Dataflow充当真正的队列。由于您的请求有结果,您可以使用TaskCompletionSource<T>使排队代码能够获得结果。 TaskCompletionSource<T>是一个“异步信号”——在本例中,我们使用它来通知调用代码他们的特定请求已通过队列并已执行。

private ActionBlock<TaskCompletionSource<int>> queue = 
    new ActionBlock<TaskCompletionSource<int>>(tcs => 
    { 
      try { tcs.TrySetResult(MyTask()); } 
      catch (Exception ex) { tcs.TrySetException(ex); } 
    }); 

每次我们发送一个TaskCompletionSource<T>对此 queue ,它将运行 MyTask()并捕获结果(无论是成功还是异常),并将这些结果传递给 TaskCompletionSource<T> .

然后我们可以像这样使用它:

public Task<int> DoMyTask() { 
  var tcs = new TaskCompletionSource<int>(); 
  queue.Post(tcs); 
  return tcs.Task; 
} 
 
public void CallMyTask() { 
  var result = await DoMyTask(); 
}