Является ли хорошей практикой использование блокировок для задач в очереди? - PullRequest
1 голос
/ 10 марта 2019

Мне нужно поместить в очередь некоторые запросы, которые могут поступить в любое время, таким образом, чтобы каждая задача начиналась только после завершения предыдущей. Вопрос в том, стоит ли использовать блокировку для этой цели? Имеет ли это какие-либо негативные последствия, и ожидаемое ли из этого поведение в очереди?

Чтобы быть более конкретным, рассмотрим код:

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 ]

3 голосов
/ 11 марта 2019

это хорошая идея использовать блокировку для этой цели?Имеет ли он какие-либо негативные последствия, и, как я ожидаю, это приведет к появлению очередей?

Блокировка не является хорошим решением.Плохие эффекты заключаются в том, что он блокирует поток пула потоков с момента, когда работа находится в очереди , до времени, когда работа завершается.Поэтому, если ваш код помещает в очередь 1000 запросов, он будет вызывать Task.Run 1000 раз и потенциально использовать это количество потоков пула потоков, каждый из которых ничего не делает, кроме ожидания блокировки.

Кроме того, блокировкине строго FIFO.Они просто в основном-сорта-FIFO.Это потому, что строго-FIFO блокировки вызывают другие проблемы, такие как конвои блокировки; ссылки в этом выпуске обсуждают "справедливость" блокировки (т. Е. Поведение FIFO) .

Итак, я рекомендую реальную очередь.Вы можете использовать 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();
}
1 голос
/ 10 марта 2019

Я думаю, что блокировка - это практически единственный способ достичь этого самостоятельно, но .NET Framework должна быть в состоянии сделать это за вас, если вы используете блокирующую коллекцию и параллельную очередь . Коллекция 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).

Можно иметь несколько потребителей для блокирования коллекции, но если вы хотите, чтобы все обрабатывалось по одному, тогда достаточно одного потребителя, как продемонстрировано.

Надеюсь, это поможет!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...