Зависимые задачи очереди C# для обработки пулом потоков
Я хочу поставить в очередь зависимые задачи в нескольких потоках, которые необходимо обработать по порядку (в каждом потоке). Потоки могут обрабатываться параллельно.
чтобы быть конкретным, скажем, мне нужно две очереди, и я хочу, чтобы задачи в каждой очереди обрабатывались по порядку. Вот пример псевдокода для иллюстрации желаемого поведения:
Queue1_WorkItem wi1a=...;
enqueue wi1a;
... time passes ...
Queue1_WorkItem wi1b=...;
enqueue wi1b; // This must be processed after processing of item wi1a is complete
... time passes ...
Queue2_WorkItem wi2a=...;
enqueue wi2a; // This can be processed concurrently with the wi1a/wi1b
... time passes ...
Queue1_WorkItem wi1c=...;
enqueue wi1c; // This must be processed after processing of item wi1b is complete
вот диаграмма со стрелками, иллюстрирующими зависимости между рабочими элементами:
в вопрос в том, как это сделать с помощью C# 4.0/.NET 4.0? Прямо сейчас у меня есть два рабочих потока, по одному на очередь, и я использую BlockingCollection<>
для каждой очереди. Вместо этого я хотел бы использовать пул потоков .NET и иметь рабочие потоки, обрабатывающие элементы одновременно (через потоки), но последовательно в потоке. Другими словами, Я хотел бы иметь возможность указать, что, например, wi1b зависит от завершения wi1a, без необходимости отслеживать завершение и помнить wi1a, когда wi1b прибывает. Другими словами, Я просто хочу скажите:"Я хочу отправить рабочий элемент для queue1, который должен обрабатываться последовательно с другими элементами, которые я уже отправил для queue1, но, возможно, параллельно с рабочими элементами, отправленными в другие очереди".
Я надеюсь, что это описание имеет смысл. Если нет, пожалуйста, не стесняйтесь задавать вопросы в комментариях, и я обновлю этот вопрос соответственно.
Спасибо за чтение.
обновление:
суммировать "дефектные" решения до сих пор, вот решения из раздела ответов, которые я не могу использовать, и причина(ы), почему я не могу их использовать:
задачи TPL требуют указания предшествующей задачи для ContinueWith()
. Я не хочу поддерживать знание предыдущей задачи каждой очереди при отправке новой задачи.
TDF ActionBlocks выглядел многообещающе, но казалось бы, что элементы, размещенные в ActionBlock, обрабатываются параллельно. Мне нужно, чтобы элементы для определенной очереди обрабатывались последовательно.
обновление 2:
RE: ActionBlocks
казалось бы, что задание MaxDegreeOfParallelism
опция для одного предотвращает параллельную обработку рабочих элементов, представленных одному ActionBlock
. Поэтому кажется, что имея ActionBlock
в очереди решает мою проблему с единственным недостатком, что это требует установки и развертывания библиотеки TDF от Microsoft, и я надеялся на чистое решение .NET 4.0. Пока это кандидат принятый ответ, если кто-то не может найти способ сделать это с чистым решением .NET 4.0, которое не вырождается в рабочий поток на очередь (который я уже использую).
4 ответов
Я понимаю, что у вас много очередей и вы не хотите связывать потоки. Вы могли бы иметь ActionBlock в очереди. ActionBlock автоматизирует большую часть того, что вам нужно: он обрабатывает рабочие элементы последовательно и запускает задачу только в ожидании работы. Если работа не отложена, задача/поток не блокируется.
лучший способ-использовать Task Parallel Library (TPL)
и Continuations
. Продолжение не только позволяет создать поток задач, но и обрабатывает исключения. Это большое введение в TPL. Но чтобы дать вам некоторое представление...
вы можете запустить задачу TPL с помощью
Task task = Task.Factory.StartNew(() =>
{
// Do some work here...
});
теперь, чтобы начать вторую задачу, когда предыдущая задача заканчивается (по ошибке или успешно), вы можете использовать ContinueWith
метод
Task task1 = Task.Factory.StartNew(() => Console.WriteLine("Antecedant Task"));
Task task2 = task1.ContinueWith(antTask => Console.WriteLine("Continuation..."));
Итак, как только task1
завершает, завершает работу или отменяется task2
'пожары и начинает работать. Обратите внимание, что if task1
было завершено до достижения второй строке кода task2
будет запланировано выполнить немедленно. The antTask
аргумент, переданный второй лямбде, является ссылкой на предшествующую задачу. См.этой ссылке для более подробных примеров...
вы также можете передать результаты продолжения из предшествующей задачи
Task.Factory.StartNew<int>(() => 1)
.ContinueWith(antTask => antTask.Result * 4)
.ContinueWith(antTask => antTask.Result * 4)
.ContinueWith(antTask =>Console.WriteLine(antTask.Result * 4)); // Prints 64.
Примечание. Обязательно читайте дальше обработка исключений в первой ссылке, так как это может привести новичка в ОСАГО в заблуждение.
последнее, что нужно посмотреть, в частности, для того, что вы хотите, это дочерние задачи. Дочерние задачи-это те, которые создаются как AttachedToParent
. В этом случае продолжение не будет выполняться, пока не будут выполнены все дочерние задачи
TaskCreationOptions atp = TaskCreationOptions.AttachedToParent;
Task.Factory.StartNew(() =>
{
Task.Factory.StartNew(() => { SomeMethod() }, atp);
Task.Factory.StartNew(() => { SomeOtherMethod() }, atp);
}).ContinueWith( cont => { Console.WriteLine("Finished!") });
надеюсь, это поможет.
Edit: вы посмотрели на ConcurrentCollections
в частности BlockngCollection<T>
. Так что в вашем случае вы может использовать что-то вроде
public class TaskQueue : IDisposable
{
BlockingCollection<Action> taskX = new BlockingCollection<Action>();
public TaskQueue(int taskCount)
{
// Create and start new Task for each consumer.
for (int i = 0; i < taskCount; i++)
Task.Factory.StartNew(Consumer);
}
public void Dispose() { taskX.CompleteAdding(); }
public void EnqueueTask (Action action) { taskX.Add(Action); }
void Consumer()
{
// This seq. that we are enumerating will BLOCK when no elements
// are avalible and will end when CompleteAdding is called.
foreach (Action action in taskX.GetConsumingEnumerable())
action(); // Perform your task.
}
}
возможно решение .NET 4.0 на основе TPL, скрывая при этом тот факт, что ему нужно где-то хранить родительскую задачу. Например:
class QueuePool
{
private readonly Task[] _queues;
public QueuePool(int queueCount)
{ _queues = new Task[queueCount]; }
public void Enqueue(int queueIndex, Action action)
{
lock (_queues)
{
var parent = _queue[queueIndex];
if (parent == null)
_queues[queueIndex] = Task.Factory.StartNew(action);
else
_queues[queueIndex] = parent.ContinueWith(_ => action());
}
}
}
это использование одной блокировки для всех очередей, чтобы проиллюстрировать идею. Однако в производственном коде я бы использовал блокировку для каждой очереди, чтобы уменьшить конкуренцию.
похоже, что дизайн, который у вас уже есть, хорош и работает. Ваши рабочие потоки (по одному на очередь) работают долго, поэтому, если вы хотите использовать вместо этого задачи, укажите TaskCreationOptions.LongRunning
таким образом, вы получаете выделенный рабочий поток.
но на самом деле нет необходимости использовать ThreadPool здесь. Это не дает много преимуществ для длительной работы.