Мне было интересно, есть ли преимущества, связанные с использованием BufferBlock, связанного с одним или несколькими ActionBlock, кроме регулирования (с использованием BoundedCapacity), вместо того, чтобы просто публиковать непосредственно в ActionBlock(s) (пока регулирование не требуется).
Преимущества использования BufferBlock‹T› в сетях передачи данных
Ответы (3)
Если все, что вы хотите сделать, это переслать элементы из одного блока в несколько других, вам не нужен BufferBlock
.
Но есть, конечно, случаи, когда это полезно. Например, если у вас есть сложная сеть потоков данных, вы можете построить ее из более мелких подсетей, каждая из которых создается собственным методом. А для этого вам нужно каким-то образом представить группу блоков. В случае, который вы упомянули, возвращение этого единственного BufferBlock
(возможно, как ITargetBlock
) из метода было бы простым решением.
Другой пример, когда BufferBlock
будет полезен, — это если вы хотите отправить элементы из нескольких исходных блоков в несколько целевых блоков. Если вы использовали BufferBlock
в качестве посредника, вам не нужно подключать каждый исходный блок к каждому целевому блоку.
Я уверен, что есть много других примеров, где вы могли бы использовать BufferBlock
. Конечно, если вы не видите смысла использовать его в вашем случае, то и не надо.
Чтобы добавить к ответу Свика, есть еще одно преимущество буферных блоков. Если у вас есть блок с несколькими выходными ссылками и вы хотите балансировать между ними, вы должны сделать выходные блоки ограниченной емкостью 1 и добавить буферный блок для обработки очереди.
Вот что мы планируем сделать:
- Некоторый блок кода будет отправлять данные в BufferBlock, используя его метод Post(T t).
- Этот BufferBlock связан с 3 экземплярами ActionBlock с помощью метода LinkTo t) BufferBlock.
Обратите внимание, что BufferBlock не передает копии входных данных всем целевым блокам, с которыми он связан. Вместо этого он делает это только с одним целевым блоком. Здесь мы ожидаем, что, когда одна цель занята обработкой запроса, он будет передан другой цели. Теперь давайте обратимся к коду ниже:
static void Main(string[] args)
{
BufferBlock<int> bb = new BufferBlock<int>();
ActionBlock<int> a1 = new ActionBlock<int>(a =>
{
Thread.Sleep(100);
Console.WriteLine("Action A1 executing with value {0}", a);
});
ActionBlock<int> a2 = new ActionBlock<int>(a =>
{
Thread.Sleep(50);
Console.WriteLine("Action A2 executing with value {0}", a);
});
ActionBlock<int> a3 = new ActionBlock<int>(a =>
{
Thread.Sleep(50);
Console.WriteLine("Action A3 executing with value {0}", a);
});
bb.LinkTo(a1);
bb.LinkTo(a2);
bb.LinkTo(a3);
Task t = new Task(() =>
{
int i = 0;
while (i < 10)
{
Thread.Sleep(50);
i++;
bb.Post(i);
}
}
);
t.Start();
Console.Read();
}
При выполнении он выдает следующий вывод:
- Действие A1 выполняется со значением 1
- Действие A1 выполняется со значением 2
- Действие A1 выполняется со значением 3
- Действие A1 выполняется со значением 4
- Действие A1 выполняется со значением 5
- Действие A1 выполняется со значением 6
- Действие A1 выполняется со значением 7
- Действие A1 выполняется со значением 8
- Действие A1 выполняется со значением 9
- Действие A1 выполняется со значением 10
Это показывает, что только одна цель фактически выполняет все данные, даже когда она занята (из-за целенаправленного добавления Thread.Sleep(100)). Почему?
Это связано с тем, что все целевые блоки по умолчанию являются жадными по своей природе и буферизуют ввод, даже если они не могут обрабатывать данные. Чтобы изменить это поведение, мы установили для параметра Bounded Capacity значение 1 в DataFlowBlockOptions при инициализации ActionBlock, как показано ниже.
static void Main(string[] args)
{
BufferBlock<int> bb = new BufferBlock<int>();
ActionBlock<int> a1 = new ActionBlock<int>(a =>
{
Thread.Sleep(100);
Console.WriteLine("Action A1 executing with value {0}", a);
}
, new ExecutionDataflowBlockOptions {BoundedCapacity = 1});
ActionBlock<int> a2 = new ActionBlock<int>(a =>
{
Thread.Sleep(50);
Console.WriteLine("Action A2 executing with value {0}", a);
}
, new ExecutionDataflowBlockOptions {BoundedCapacity = 1});
ActionBlock<int> a3 = new ActionBlock<int>(a =>
{
Thread.Sleep(50);
Console.WriteLine("Action A3 executing with value {0}", a);
}
, new ExecutionDataflowBlockOptions {BoundedCapacity = 1});
bb.LinkTo(a1);
bb.LinkTo(a2);
bb.LinkTo(a3);
Task t = new Task(() =>
{
int i = 0;
while (i < 10)
{
Thread.Sleep(50);
i++;
bb.Post(i);
}
});
t.Start();
Console.Read();
}
Вывод этой программы:
- Действие A1 выполняется со значением 1
- Действие A2 выполняется со значением 3
- Действие A1 выполняется со значением 2
- Действие A3 выполняется со значением 6
- Действие A3 выполняется со значением 7
- Действие A3 выполняется со значением 8
- Действие A2 выполняется со значением 5
- Действие A3 выполняется со значением 9
- Действие A1 выполняется со значением 4
- Действие A2 выполняется со значением 10
Это явное распределение данных по трем ActionBlock, как и ожидалось.
Нет, второй пример не скомпилируется по ряду причин: установить greedy=false можно только для «группирующего» блока потока данных, но не для исполнительного блока; и тогда его нужно установить через GroupingDataflowBlockOptions, а не через DataflowBlockOptions; а затем он устанавливается как значение свойства "{Жадный = false}", а не параметр конструктора.
Если вы хотите ограничить емкость блока действий, сделайте это, установив значение свойства BoundedCapacity DataflowBlockOptions (хотя, как заявил OP, они уже знают об этой опции). Как это:
var a1 = new ActionBlock<int>(
i => doSomeWork(i),
new ExecutionDataflowBlockOptions {BoundedCapacity = 1}
);