Как удалить задачу из коллекции после завершения

#c# #.net-4.0 #task-parallel-library

#c# #.net-4.0 #задача-параллельная-библиотека

Вопрос:

Допустим, у меня есть коллекция System.Threading.Tasks.Task :

 HashSet<Task> myTasks = new HashSet<Task>();
  

… и я периодически добавляю больше в коллекцию, поскольку у меня есть больше данных, которые необходимо обработать:

 foreach (DataItem item in itemsToProcess)
    myTasks.Add(
        Task.Factory.StartNew(
            () => Process(item),
            cancellationToken,
            TaskCreationOptions.LongRunning,
            TaskScheduler.Default));    
  

Поскольку Task s остаются в TaskStatus.RanToCompletion состоянии после завершения, а не просто исчезают, они будут оставаться в коллекции до явного удаления, и коллекция будет расти бесконечно. Task s необходимо обрезать, чтобы предотвратить это.

Один из подходов, который я рассмотрел, заключается в предоставлении Task доступа к коллекции и ее удалении в самом конце. Но я также рассматриваю архитектуру, в которой мне пришлось бы удалить задачу, которую не создал мой компонент. Моя первая мысль — прикрепить триггер или событие к завершению каждой задачи, что-то вроде этого:

 foreach (Task task in createdSomewhereElse)
{
    lock (myTasks) myTasks.Add(task);
    task.WhenTaskIsCompleted  = 
        (o, ea) => { lock(myTasks) myTasks.Remove(task); };
    task.Start();
}
  

…но Task такого события нет. Есть ли какой-нибудь хороший способ выполнить то, что я ищу? Что-то вроде этого:

Ответ №1:

Вы, конечно, можете прикрепить триггер для завершения задачи: Task.ContinueWith (и его общий эквивалент). Вероятно, этого было бы достаточно для вас.

Возможно, вы также захотите использовать ConcurrentDictionary как своего рода параллельный набор для бедных — таким образом, вам не придется блокировать доступ к коллекции. Просто используйте Keys свойство при повторении и используйте все, что вам нравится, в качестве значения.

Комментарии:

1. Джон, ты имеешь в виду конкретную реализацию ConcurrentHashSet ? Насколько мне известно, в BCL ничего не встроено.

2. @LukeH: Doh — Я думал о ConcurrentDictionary. Отредактирует 🙂

3. Похоже, это именно то, что я хочу сделать. 🙂 Итак, замена = инструкции на task.ContinueWith((t) => { lock(myTasks) myTasks.Remove(t); }); правильная? Кроме того, 1 к комментарию @LukeH; Я не видел ни одного из них в системных библиотеках.

4. Ааа, на секунду разволновался… На самом деле я использую ConcurrentDictionary в реальном коде, но абстрагировался от HashSet, чтобы уменьшить сложность вопроса.

5. @Calvin: Для меня это выглядит правильно, да. ContinueWith — это одна из тех вещей, на которых построена поддержка асинхронности в C # 5 🙂

Ответ №2:

Почему вам нужно хранить задачи в коллекции?

Почему бы не использовать решение, основанное на BlockingCollection и Parallel.ForEach

 var sources = new BlockingCollection<DataItem>();

Task.Factory.StartNew(() => {
    Parallel.ForEach(sources.GetConsumingPartitioner(),
                     item => Process(item));
});
  

Теперь вы можете просто поместить свои элементы в блокирующую коллекцию, и они будут обработаны автоматически.

 foreach (DataItem item in itemsToProcess)
    sources.Add(item);
  

Вы можете использовать sources.Count и foreach (DataItem item in sources) для просмотра необработанных элементов.
(Отличие от вашего решения в том, что вы не можете видеть элементы, обрабатываемые в данный момент)

Комментарии:

1. Интересная альтернатива. Я сохраняю задачи в коллекции, чтобы я мог видеть, сколько из них все еще обрабатываются, дождаться завершения всех текущих задач обработки ( Task.WaitAll(myTasks.ToArray()); ), а в реальной жизни, когда я использую ConcurrentDictionary, связать элемент данных с задачей, которая его обрабатывает.

2. @adrianm, есть ли какое-либо ожидание отмены конкретной задачи?

3. @Calvin, ничего встроенного. Параллельно. ForEach получил некоторые перегрузки, которые позволяют отменить оставшиеся задачи. Я бы добавил флаг или токен отмены в элемент данных и проверил отмену в Process ()

4. Используя Parallel. Выполнение каждого из блокирующих наборов опасно: blogs.msdn.com/b/pfxteam/archive/2010/04/06/9990420.aspx

5. @Ronnie: Если вы прочитаете свою собственную статью по ссылке, вы увидите, что решение заключается в использовании .GetConsumingPartitioner вместо .GetConsumingEnumerable . Затем посмотрите на мой код выше.

Ответ №3:

Используйте ContinueWith, чтобы задать действие, которое удаляет задачу из набора.