У меня есть количественная и повторяемая проблема с использованием параллельной библиотеки задач BlockingCollection<T>
, ConcurrentQueue<T>
и GetConsumingEnumerable
при попытке создать простой конвейер.
Вкратце, добавление записей в значение по умолчанию BlockingCollection<T>
(которое под капотом опирается на ConcurrentQueue<T>
) из одного потока, не гарантирует, что они будут удалены из BlockingCollection<T>
из другого потока, вызывающего GetConsumingEnumerable()
Метод.
Я создал очень простое приложение Winforms для воспроизведения/имитации этого, которое просто печатает целые числа на экране.
-
Timer1
отвечает за очередность рабочих элементов... Он использует параллельный словарь под названием_tracker
, чтобы он знал, что он уже добавил в блокирующую коллекцию. -
Timer2
просто регистрирует состояние счета какBlockingCollection
, так и_tracker
- Кнопка START запускает
Paralell.ForEach
, которая просто выполняет итерации по блокирующим коллекциямGetConsumingEnumerable()
и начинает печатать их во втором списке. - Кнопка STOP останавливает
Timer1
, предотвращая добавление дополнительных записей в коллекцию блокировки.
public partial class Form1 : Form
{
private int Counter = 0;
private BlockingCollection<int> _entries;
private ConcurrentDictionary<int, int> _tracker;
private CancellationTokenSource _tokenSource;
private TaskFactory _factory;
public Form1()
{
_entries = new BlockingCollection<int>();
_tracker = new ConcurrentDictionary<int, int>();
_tokenSource = new CancellationTokenSource();
_factory = new TaskFactory();
InitializeComponent();
}
private void timer1_Tick(object sender, EventArgs e)
{ //ADDING TIMER -> LISTBOX 1
for(var i = 0; i < 3; i++,Counter++)
{
if (_tracker.TryAdd(Counter, Counter))
_entries.Add(Counter);
listBox1.Items.Add(string.Format("Adding {0}", Counter));
}
}
private void timer2_Tick_1(object sender, EventArgs e)
{ //LOGGING TIMER -> LIST BOX 3
listBox3.Items.Add(string.Format("Tracker Count : {0} / Entries Count : {1}", _tracker.Count, _entries.Count));
}
private void button1_Click(object sender, EventArgs e)
{ //START BUTTON -> LOGS TO LIST BOX 2
var options = new ParallelOptions {
CancellationToken = _tokenSource.Token,
MaxDegreeOfParallelism = 1
};
_factory.StartNew(() => { Parallel.ForEach(_entries.GetConsumingEnumerable(), options, DoWork); });
timer1.Enabled = timer2.Enabled = true;
timer1.Start();
timer2.Start();
}
private void DoWork(int entry)
{
Thread.Sleep(1000); //Sleep for 1 second to simulate work being done.
Invoke((MethodInvoker)(() => listBox2.Items.Add(string.Format("Processed {0}", entry))));
int oldEntry;
_tracker.TryRemove(entry, out oldEntry);
}
private void button2_Click(object sender, EventArgs e)
{ //STOP BUTTON
timer1.Stop();
timer1.Enabled = false;
}
Здесь последовательность событий:
- Нажмите "Пуск"
- Timer1 ticks и ListBox1 сразу обновляются 3 сообщениями (добавление 0, 1, 2)
- ListBox2 затем обновляется с 3 сообщениями, на 1 секунду
- Обработка 0
- Обработка 1
- Обработка 2
- Timer1 ticks и ListBox1 сразу обновляются 3 сообщениями (добавление 3, 4, 5)
- ListBox2 обновляется с 2 сообщениями, 1 секунда
- Обработка 3
- Обработка 4
- Обработка 5 не печатается... казалось бы, пропала без вести.
- Нажмите STOP, чтобы предотвратить добавление дополнительных сообщений по таймеру 1
- Подождите... "Обработка 5" все еще не отображается
Вы можете видеть, что параллельный словарь по-прежнему отслеживает, что 1 элемент еще не обработан и впоследствии удален из _tracker
Если я снова нажму "Старт", тогда таймер 1 начнет добавлять еще 3 записи, а цикл "Параллельный" вернется к пожизненной печати 5, 6, 7 и 8.
У меня полная потеря, почему это происходит. Очевидно, что при вызове start явно вызывает newtask, который вызывает Paralell foreach и повторно выполняет GetConsumingEnumerable(), который волшебным образом обнаруживает недостающую запись... I
Почему BlockingCollection.GetConsumingEnumerable()
не гарантирует итерацию по каждому элементу, добавленному в коллекцию.
Почему добавление большего количества записей впоследствии приводит к тому, что он "отклеивается" и продолжает обрабатывать?