生产者消费者模式之BlockingCollecting并行通道
1. 场景
假设我们有一些数据需要经历数个计算阶段,并且这些计算需要花费大量时间。后者计算需要使用前者的结果,所以不能并行运行他。 如果只有一项要处理,则可能很难改进性能。然而,如果许多项需要经历相同的计算阶段,我们可以使用并行管道技术。这就意味着我们不用等待所有项通过第一个计算阶段,然后才进行第二个。其实只需要一个项完成计算阶段后,就可以将其移动到下一个阶段,同时接下来的项可以被之前的阶段处理,依次类推。结果,通过移动第一个项在第一个计算阶段的时间来实现一个几乎是并行的处理过程。
2. 实例说明
假设有这么一个场景,我们为每个处理阶段使用了4个集合,这里说明我们可以以并行的方式处理每个阶段。我们做的第一步是提供了取消整个过程的功能(按下C键取消)。我们创建了一个取消标志,并运行一个单独的任务来监视C键。然后我们自定义了管道。它有三个主要的阶段组成。第一个阶段是将初始值放入到前四个集合中,其将被作为数据源被下一个阶段使用。该代码在Parallel.For循环中使用,而该循环在Paralle.Invoke声明中。由于我们以并行的方式运行所以阶段,所以这阶段也是以并行的方式运行。
接下来的阶段是定义管道元素。该逻辑定义在PipelineWorker类中。我们使用输入集合初始化了工作者,也可以称为过滤器,因为它们过滤了初始序列。其中一个将整数转换为小数,第二个将小数转换为字符串。最后一个工作者只将每个传入的字符串打印到控制台即可。每个地方都提供了运行的线程ID,来揭示整个程序是如何工作的。此外,我们添加了人为的延迟,所以每项的处理更加自然,就像我们真的需要大计算一样。
结果我们得到了实际的期望行为。首先,一些项被创建在初始集合中。接下来,第一个过滤器可以处理它们,待处理完毕后,第二个处理器也开始工作,最终该项被传给最后一个工作者并将打印到控制台
3. 关系图
3. 实例代码
class Program
{
/// <summary>
/// BlockingCollection 实例数
/// </summary>
private const int CollectionsNumber = 40;
/// <summary>
/// BlockingCollection 实例最大容量容量
/// 控制内存中的集合的最大大小,并可防止生产者占用过多资源
/// </summary>
private const int Count = 10;
static void Main(string[] args)
{
//BlockingCollection 支持 CancellationToken 取消任务
var cts = new CancellationTokenSource();
Task.Run(() =>
{
if (Console.ReadKey().KeyChar == 'c')
cts.Cancel();
});
var sourceArrays = new BlockingCollection<int>[CollectionsNumber];
for (int i = 0; i < sourceArrays.Length; i++)
{
sourceArrays[i] = new BlockingCollection<int>(Count);
}
var filter1 = new PipelineWorker<int, decimal>
(
sourceArrays,
(n) => Convert.ToDecimal(n * 0.97),
cts.Token,
"filter1"
);
var filter2 = new PipelineWorker<decimal, string>
(
filter1.Output,
(s) => String.Format("--{0}--", s),
cts.Token,
"filter2"
);
var filter3 = new PipelineWorker<string, string>
(
filter2.Output,
(s) => Console.WriteLine("The final result is {0} on thread id {1}",
s, Thread.CurrentThread.ManagedThreadId),
cts.Token,
"filter3"
);
try
{
Parallel.Invoke(
() =>
{
Parallel.For(0, sourceArrays.Length * Count, (j, state) =>
{
if (cts.Token.IsCancellationRequested)
{
state.Stop();
}
//向其添加项的集合在 collections 数组中的索引;如果未能添加项,则为 -1。
int k = BlockingCollection<int>.TryAddToAny(sourceArrays, j);
if (k >= 0)
{
//延时处理,模式真实计算处理
Console.WriteLine("added {0} to source data on thread id {1}", j, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromMilliseconds(100));
}
});
//生产者可以调用 CompleteAdding 方法来指示不再添加项
foreach (var arr in sourceArrays)
{
arr.CompleteAdding();
}
},
() => filter1.Run(),
() => filter2.Run(),
() => filter3.Run()
);
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
Console.WriteLine(ex.Message + ex.StackTrace);
}
if (cts.Token.IsCancellationRequested)
{
Console.WriteLine("Operation has been canceled! Press ENTER to exit.");
}
else
{
Console.WriteLine("Press ENTER to exit.");
}
Console.ReadLine();
}
/// <summary>
/// 管道数据源
/// </summary>
/// <typeparam name="TInput">Input</typeparam>
/// <typeparam name="TOutput">Output</typeparam>
class PipelineWorker<TInput, TOutput>
{
Func<TInput, TOutput> _processor = null;
Action<TInput> _outputProcessor = null;
BlockingCollection<TInput>[] _input;
CancellationToken _token;
public PipelineWorker(
BlockingCollection<TInput>[] input,
Func<TInput, TOutput> processor,
CancellationToken token,
string name)
{
_input = input;
Output = new BlockingCollection<TOutput>[_input.Length];
for (int i = 0; i < Output.Length; i++)
Output[i] = null == input[i] ? null : new BlockingCollection<TOutput>(Count);
_processor = processor;
_token = token;
Name = name;
}
public PipelineWorker(
BlockingCollection<TInput>[] input,
Action<TInput> renderer,
CancellationToken token,
string name)
{
_input = input;
_outputProcessor = renderer;
_token = token;
Name = name;
Output = null;
}
public BlockingCollection<TOutput>[] Output { get; private set; }
public string Name { get; private set; }
/// <summary>
/// 消费者监视 IsCompleted 属性以了解集合何时为空,持续等待生产者
/// </summary>
public void Run()
{
Console.WriteLine("{0} is running", this.Name);
while (!_input.All(bc => bc.IsCompleted) && !_token.IsCancellationRequested)
{
TInput receivedItem;
//从其中移除项的集合在 collections 数组中的索引;如果未能移除项,则为 -1。
int i = BlockingCollection<TInput>.TryTakeFromAny(
_input, out receivedItem, 50, _token);
if (i >= 0)
{
if (Output != null)
{
TOutput outputItem = _processor(receivedItem);
BlockingCollection<TOutput>.AddToAny(Output, outputItem);
Console.WriteLine("{0} sent {1} to next, on thread id {2}",
Name, outputItem, Thread.CurrentThread.ManagedThreadId);
//延时处理,模式真实计算处理
Thread.Sleep(TimeSpan.FromMilliseconds(100));
}
else
{
_outputProcessor(receivedItem);
}
}
else
{
Thread.Sleep(TimeSpan.FromMilliseconds(50));
}
}
if (Output != null)
{
foreach (var bc in Output) bc.CompleteAdding();
}
}
}
}
PS:BlockingCollection 可以指定使用的集合类型,先进先出(FIFO)行为可以使用 ConcurrentQueue 对象;后进先出(LIFO)行为可以使用 ConcurrentStack 对象。 也可以使用 IProducerConsumerCollection 接口的任何集合类。(默认是使用ConcurrentQueue )