使用锁来排队任务是一种好习惯吗?

时间:2019-03-10 10:13:47

标签: c# locking

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

更具体地说,请考虑以下代码:

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()将在任何时间(可能同时被调用)

2 个答案:

答案 0 :(得分:3)

  

为此目的使用锁定是个好主意吗?它会产生任何不良影响吗?我期望的排队行为是否会因此而导致?

在这里锁定不是一个好的解决方案。不利的影响是,从工作进入队列之时起直到工作完成为止,它都会阻塞线程池线程。因此,如果您的代码将1000个请求排队,则它将调用Task.Run 1000次,并可能耗尽该数量的线程池线程,每个线程池除了等待锁外什么都不做。

此外,锁也不是严格的FIFO。它们只是多数排序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>是一个“异步信号”-在这种情况下,我们正在使用它来通知调用代码其特定的请求已通过队列并已执行。

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

每次我们向此TaskCompletionSource<T>发送queue时,它将运行MyTask()并捕获结果(无论是成功还是异常),并将这些结果传递给{{ 1}}。

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

TaskCompletionSource<T>

答案 1 :(得分:1)

我认为锁定几乎是您自己实现此目标的唯一方法,但是.NET框架应该可以在您使用blocking collectionconcurrent queue的情况下为您完成此任务。 Blocking集合为您提供了线程安全的生产者/消费者模式的实现。

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


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

这可能不是一个完美的例子,但是我敢肯定您的想法。生产者/消费者包装并发队列,因此任务以先进先出(FIFO)的方式执行。

可能有多个消费者用于阻塞收集,但是如果您希望一次处理一件商品,那么演示一个单一的消费者就足够了。

希望对您有帮助!