简体   繁体   English

以FIFO顺序C#的TPL生产者消费者

[英]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: 我了解的情况是这样的:

  1. You have a BlockingCollection that is full 您的BlockingCollection已满
  2. A number of threads start, each trying to add to the BlockingCollection. 启动了多个线程,每个线程都试图添加到BlockingCollection中。 These calls will all block; 这些呼叫将全部阻塞; that is why they need to occur in parallel. 这就是为什么它们需要并行发生的原因。
  3. As space becomes available, the Add calls will become unblocked. 当有可用空间时,添加呼叫将被解除阻止。
  4. The calls to 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM