对队列任务使用锁是一种好习惯吗?

Is it a good practice to use locks for queuing tasks?

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

更具体地说,考虑代码:

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() 将随时调用,可能同时调用。

我认为锁定几乎是您自己实现此目的的唯一方法,但如果您使用 blocking collection and a concurrent queue,.NET 框架应该能够为您完成此操作。 Blocking 集合为您提供了线程安全的 producer/consumer 模式的实现。

这是一个按顺序打印数字的例子。


class Program
{
    private static BlockingCollection<Task> m_BlockingCollection = new BlockingCollection<Task>(new ConcurrentQueue<Task>());
    private static int Counter;

    static async Task Main(string[] args)
    {
        Task.Run(ProcessQueue); //Don't await for this demo!
        Task.Run(AddStuffToQueue); //Don't await for this demo!

        Console.ReadLine();
        m_BlockingCollection.CompleteAdding();
        while (!m_BlockingCollection.IsAddingCompleted)
            Thread.Sleep(5);
    }

    private static void AddStuffToQueue()
    {
        while(true)
            m_BlockingCollection.Add(new Task(() => Console.WriteLine(Interlocked.Increment(ref Counter))));
    }

    private static async Task ProcessQueue()
    {
        while (!m_BlockingCollection.IsCompleted && m_BlockingCollection.TryTake(out Task task))
            ProcessTask(task);
    }

    private static void ProcessTask(Task task)
    {
        task.RunSynchronously();
    }
}

这可能不是一个完美的例子,但我相信你明白了。 producer/consumer 包装并发队列,因此任务按先进先出 (FIFO) 执行。

阻塞集合可以有多个消费者,但如果您希望一次处理一个事物,那么只需要一个消费者就足够了。

希望对您有所帮助!

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).

所以,我推荐一个真正的队列。您可以使用 TPL Dataflow 中的 ActionBlock<T> 作为真正的队列。由于您的请求有 个结果 ,您可以使用 TaskCompletionSource<T> 作为入队代码以获得结果。 TaskCompletionSource<T> 是一个 "asynchronous signal" - 在这种情况下,我们使用它来通知调用代码 他们的特定 请求已通过队列并已执行。

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();
}