[英]TPL Producer Consumer in a FIFO order C#
I'm limited to .NET 3.5 and I'm using TPL. 我仅限于.NET 3.5,并且正在使用TPL。 The scenario is producer-consumer but there is no problem of blocking. 该方案是生产者-消费者,但是没有阻塞的问题。 PLINQ cannot be used in this scenario (because of limitations) and what we want to achieve is the fastest way to produce many items (where each production is a long-running one, and the number of items exceeds 100,000) but each item must be consumed in a FIFO order (which means, the first item I asked to be produced must be consumed first, even if it was created after other items) and also consumed as fast as possible. 由于种种限制,不能在这种情况下使用PLINQ,而我们想要实现的是生产许多项目的最快方法(其中每项生产都是长期运行的,并且项目数超过100,000),但是每项必须以FIFO顺序消费(这意味着,我要求生产的第一个商品必须首先被消费,即使它是在其他商品之后创建的)也必须尽快消费。
For this problem I tried using a task list, wait for the first item in the list to be completed (taskList.First().IsCompleted()) and then using the consuming function on it, but for some reason I seem to run out of memory (maybe too many items in the task list because of tasks waiting to start?) Is there any better way to do that? 对于这个问题,我尝试使用任务列表,等待列表中的第一项完成(taskList.First()。IsCompleted()),然后在其上使用消耗函数,但是由于某些原因,我似乎用光了内存(可能由于等待启动的任务而导致任务列表中的项目过多?)还有更好的方法吗? (I'm trying to achieve the fastest possible) (我正在努力实现最快的速度)
Many thanks! 非常感谢!
OK after the edit - instead of adding the results in the BlockingCollection, add the Tasks in the blocking collection. 编辑后确定-将结果添加到BlockingCollection中,而不是将结果添加到BlockingCollection中。 This has the feature where the items are processed in order AND there is a maximum parallelism which will prevent too many threads from kicking off and you eating up all your memory. 它具有按顺序处理项目的功能,并且具有最大的并行度,可以防止过多的线程启动,并且您会耗尽所有内存。
https://dotnetfiddle.net/lUbSqB https://dotnetfiddle.net/lUbSqB
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
public class Program
{
private static BlockingCollection<Task<int>> BlockingCollection {get;set;}
public static void Producer(int numTasks)
{
Random r = new Random(7);
for(int i = 0 ; i < numTasks ; i++)
{
int closured = i;
Task<int> task = new Task<int>(()=>
{
Thread.Sleep(r.Next(100));
Console.WriteLine("Produced: " + closured);
return closured;
});
BlockingCollection.Add(task);
task.Start();
}
BlockingCollection.CompleteAdding();
}
public static void Main()
{
int numTasks = 20;
int maxParallelism = 3;
BlockingCollection = new BlockingCollection<Task<int>>(maxParallelism);
Task.Factory.StartNew(()=> Producer(numTasks));
foreach(var task in BlockingCollection.GetConsumingEnumerable())
{
task.Wait();
Console.WriteLine(" Consumed: "+ task.Result);
task.Dispose();
}
}
}
And the results: 结果:
Produced: 0
Consumed: 0
Produced: 1
Consumed: 1
Produced: 3
Produced: 2
Consumed: 2
Consumed: 3
Produced: 4
Consumed: 4
Produced: 6
Produced: 5
Consumed: 5
Consumed: 6
Produced: 7
Consumed: 7
Produced: 8
Consumed: 8
Produced: 10
Produced: 9
Consumed: 9
Consumed: 10
Produced: 12
Produced: 13
Produced: 11
Consumed: 11
Consumed: 12
Consumed: 13
Produced: 15
Produced: 14
Consumed: 14
Consumed: 15
Produced: 17
Produced: 16
Produced: 18
Consumed: 16
Consumed: 17
Consumed: 18
Produced: 19
Consumed: 19
I thought this was an interesting question so I spent a bit of time on it. 我认为这是一个有趣的问题,所以我花了一些时间。
The scenario I understand it is this: 我了解的情况是这样的:
Add
need to complete in the order they were received. 对Add
的调用需要按照收到的顺序完成。 First of all, let's talk about code structure. 首先,让我们谈谈代码结构。 Instead of using a BlockingCollection and writing procedural code around it, I suggest extending the BlockingCollection and replacing the Add method with the functionality you need. 建议不要扩展BlockingCollection并使用所需的功能替换Add方法,而不要使用BlockingCollection并在其周围编写过程代码。 It may look something like this: 它可能看起来像这样:
public class QueuedBlockingCollection<T> : BlockingCollection<T>
{
private FifoMonitor monitor = new FifoMonitor();
public QueuedBlockingCollection(int max) : base (max) {}
public void Enqueue(T item)
{
using (monitor.Lock())
{
base.Add(item);
}
}
}
Here, the trick is the use of a FifoMonitor
class, which will give you the functionality of a lock
but will enforce order. 在这里,技巧是使用FifoMonitor
类,该类将为您提供lock
的功能,但将强制执行顺序。 Unfortunately, no class like that exists in the CLR. 不幸的是,CLR中没有这样的类。 But we can write one : 但是我们可以写一个 :
public class FifoMonitor
{
public class FifoCriticalSection : IDisposable
{
private readonly FifoMonitor _parent;
public FifoCriticalSection(FifoMonitor parent)
{
_parent = parent;
_parent.Enter();
}
public void Dispose()
{
_parent.Exit();
}
}
private object _innerLock = new object();
private volatile int counter = 0;
private volatile int current = 1;
public FifoCriticalSection Lock()
{
return new FifoCriticalSection(this);
}
private void Enter()
{
int mine = Interlocked.Increment(ref counter);
Monitor.Enter(_innerLock);
while (current != mine) Monitor.Wait(_innerLock);
}
private void Exit()
{
Interlocked.Increment(ref current);
Monitor.PulseAll(_innerLock);
Monitor.Exit(_innerLock);
}
}
Now to test. 现在进行测试。 Here's my program: 这是我的程序:
public class Program
{
public static void Main()
{
//Setup
var blockingCollection = new QueuedBlockingCollection<int>(10);
var tasks = new Task[10];
//Block the collection by filling it up
for (int i=1; i<=10; i++) blockingCollection.Add(99);
//Start 10 threads all trying to add another value
for (int i=1; i<=10; i++)
{
int index = i; //unclose
tasks[index-1] = Task.Run( () => blockingCollection.Enqueue(index) );
Task.Delay(100).Wait(); //Wait long enough for the Enqueue call to block
}
//Purge the collection, making room for more values
while (blockingCollection.Count > 0)
{
var n = blockingCollection.Take();
Console.WriteLine(n);
}
//Wait for our pending adds to complete
Task.WaitAll(tasks);
//Display the collection in the order read
while (blockingCollection.Count > 0)
{
var n = blockingCollection.Take();
Console.WriteLine(n);
}
}
}
Output: 输出:
99
99
99
99
99
99
99
99
99
99
1
2
3
4
5
6
7
8
9
10
Looks like it works! 看起来很有效! But just to be sure, I changed Enqueue
back to Add
, to ensure that the solution actually does something. 但是可以肯定的是,我将Enqueue
改回Add
,以确保该解决方案确实可以完成某些工作。 Sure enough, it ends up out of order with the regular Add
. 果然,它最终与常规Add
。
99
99
99
99
99
99
99
99
99
99
2
3
4
6
1
5
7
8
9
10
Check out the code on DotNetFiddle 在DotNetFiddle上查看代码
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.