CPL生产者消费者以FIFO顺序C#

时间:2018-02-01 21:31:44

标签: c# multithreading task-parallel-library producer-consumer

我只限于.NET 3.5而且我正在使用TPL。 该场景是生产者 - 消费者,但没有阻塞问题。 PLINQ不能在这种情况下使用(由于限制),我们想要实现的是生产许多物品的最快方式(每个生产都是长期生产的,物品数超过100,000个),但每个项目必须是以FIFO顺序消耗(这意味着,我要求生成的第一个项目必须首先被消费,即使它是在其他项目之后创建的)并且也尽可能快地消耗。

对于这个问题,我尝试使用任务列表,等待列表中的第一项完成(taskList.First()。IsCompleted()),然后使用消费函数,但由于某种原因,我似乎内存不足(由于任务等待启动,任务列表中的项目可能过多?)有没有更好的方法呢? (我正在努力实现最快的速度)

非常感谢!

2 个答案:

答案 0 :(得分:4)

编辑后确定 - 而不是在BlockingCollection中添加结果,在阻止集合中添加任务。这具有按顺序处理项目的功能,并且存在最大的并行性,这将防止过多的线程开始并且您占用了所有内存。

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();
        }

    }
}

结果:

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

答案 1 :(得分:3)

我认为这是一个有趣的问题,所以我花了一点时间。

我理解的情况是:

  1. 您有一个已满的BlockingCollection
  2. 启动了许多线程,每个线程都尝试添加到BlockingCollection。这些电话都会阻止;这就是他们需要并行发生的原因。
  3. 当空间可用时,添加呼叫将被取消阻止。
  4. Add的调用需要按照收到的顺序完成。
  5. 首先,我们来谈谈代码结构。我建议扩展BlockingCollection并用你需要的功能替换Add方法,而不是使用BlockingCollection并编写程序代码。它可能看起来像这样:

    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);
            }
        }
    }
    

    这里的诀窍是使用FifoMonitor类,它将为您提供lock的功能,但会强制执行命令。不幸的是,CLR中不存在类似的类。但我们可以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);
        }
    }
    

    现在进行测试。这是我的计划:

    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);
            }
    
        }
    }
    

    输出:

    99
    99
    99
    99
    99
    99
    99
    99
    99
    99
    1
    2
    3
    4
    5 
    6
    7
    8
    9
    10
    

    看起来很有效!但只是为了确定,我将Enqueue更改回Add,以确保解决方案实际上有所作为。果然,它最终与常规Add无关。

    99
    99
    99
    99
    99
    99
    99
    99
    99
    99
    2
    3
    4
    6
    1
    5
    7
    8
    9
    10
    

    查看DotNetFiddle

    上的代码