如何在多个线程中并行运行相关任务?

时间:2013-04-25 07:50:30

标签: c# .net multithreading parallel-processing task

我有一套巨大的玩家要用c#执行。每次计算都会生成一个结果数据,我想写入一个文件(我正在使用SQLite)。目前我正在以这样的顺序方式这样做[Task1 - > FileSaving1],[Task2 - > FileSaving2] ,.等等。

但我的首要任务是首先完成所有计算,所以我想在一个线程中并行运行计算,并保存文件在另一个线程中完成。每次计算结束并准备好写入数据时,都会发出FileSaving线程的信号。 FileSaving可以是顺序的也可以是并行的。

如何在C#中实现这一目标?我使用的是.Net 4.0。 如果可能的话,请给我一些例子。

1 个答案:

答案 0 :(得分:2)

您可以使用BlockingCollection<T>来帮助解决此问题。

棘手的是你需要几个线程来处理工作项,但是它们可以以随机顺序产生它们的输出,因此你需要在写入时多路复用输出(假设你想以与它相同的顺序写入数据)如果您使用旧的单线程解决方案,则会被写入。

I wrote a class to do this a while back

它假定您可以将每个“工作项”封装在类的实例中。这些实例被添加到工作队列中;然后多个线程(通过Task)可以从工作队列中删除工作项,处理它们,然后将它们输出到优先级队列。

最后,另一个线程可以从已完成的队列中删除已完成的工作项,小心地将它们复用,以便按照与最初添加到工作队列中相同的顺序删除项目。

此实现为您创建和管理线程。您需要告诉它要使用多少个工作线程,并为其提供代理以提供新的工作项(Read()),处理每个工作项(Process())并输出每个工作项({{1} })。

多个线程只调用Write()委托。

请注意,如果您不关心订单,可以避免所有这些内容,并且几乎直接使用Process()

以下是代码:

BlockingCollection

以下是测试代码:

public sealed class ParallelWorkProcessor<T> where T: class // T is the work item type.
{
    public delegate T    Read();           // Called by only one thread.
    public delegate T    Process(T block); // Called simultaneously by multiple threads.
    public delegate void Write(T block);   // Called by only one thread.

    public ParallelWorkProcessor(Read read, Process process, Write write, int numWorkers = 0)
    {
        _read    = read;
        _process = process;
        _write   = write;

        numWorkers = (numWorkers > 0) ? numWorkers : Environment.ProcessorCount;

        _workPool    = new SemaphoreSlim(numWorkers*2);
        _inputQueue  = new BlockingCollection<WorkItem>(numWorkers);
        _outputQueue = new ConcurrentPriorityQueue<int, T>();
        _workers     = new Task[numWorkers];

        startWorkers();
        Task.Factory.StartNew(enqueueWorkItems);
        _multiplexor = Task.Factory.StartNew(multiplex);
    }

    private void startWorkers()
    {
        for (int i = 0; i < _workers.Length; ++i)
        {
            _workers[i] = Task.Factory.StartNew(processBlocks);
        }
    }

    private void enqueueWorkItems()
    {
        int index = 0;

        while (true)
        {
            T data = _read();

            if (data == null) // Signals end of input.
            {
                _inputQueue.CompleteAdding();
                _outputQueue.Enqueue(index, null); // Special sentinel WorkItem .
                break;
            }

            _workPool.Wait();
            _inputQueue.Add(new WorkItem(data, index++));
        }
    }

    private void multiplex()
    {
        int index = 0; // Next required index.
        int last = int.MaxValue;

        while (index != last)
        {
            KeyValuePair<int, T> workItem;
            _outputQueue.WaitForNewItem(); // There will always be at least one item - the sentinel item.

            while ((index != last) && _outputQueue.TryPeek(out workItem))
            {
                if (workItem.Value == null) // The sentinel item has a null value to indicate that it's the sentinel.
                {
                    last = workItem.Key;  // The sentinel's key is the index of the last block + 1.
                }
                else if (workItem.Key == index) // Is this block the next one that we want?
                {
                    // Even if new items are added to the queue while we're here, the new items will be lower priority.
                    // Therefore it is safe to assume that the item we will dequeue now is the same one we peeked at.

                    _outputQueue.TryDequeue(out workItem);
                    Contract.Assume(workItem.Key == index); // This *must* be the case.
                    _workPool.Release();                    // Allow the enqueuer to queue another work item.
                    _write(workItem.Value);
                    ++index;
                }
                else // If it's not the block we want, we know we'll get a new item at some point.
                {
                    _outputQueue.WaitForNewItem();
                }
            }
        }
    }

    private void processBlocks()
    {
        foreach (var block in _inputQueue.GetConsumingEnumerable())
        {
            var processedData = _process(block.Data);
            _outputQueue.Enqueue(block.Index, processedData);
        }
    }

    public bool WaitForFinished(int maxMillisecondsToWait) // Can be Timeout.Infinite.
    {
        return _multiplexor.Wait(maxMillisecondsToWait);
    }

    private sealed class WorkItem
    {
        public WorkItem(T data, int index)
        {
            Data  = data;
            Index = index;
        }

        public T   Data  { get; private set; }
        public int Index { get; private set; }
    }

    private readonly Task[] _workers;
    private readonly Task _multiplexor;
    private readonly SemaphoreSlim _workPool;
    private readonly BlockingCollection<WorkItem> _inputQueue;
    private readonly ConcurrentPriorityQueue<int, T> _outputQueue;
    private readonly Read    _read;
    private readonly Process _process;
    private readonly Write   _write;
}

这也需要ConcurrentPriorityQueue implementation from here

我不得不稍微修改一下,所以这是我的修改版本:

namespace Demo
{
    public static class Program
    {
        private static void Main(string[] args)
        {
            _rng = new Random(34324);

            int threadCount = 8;
            _maxBlocks = 200;
            ThreadPool.SetMinThreads(threadCount + 2, 4); // Kludge to prevent slow thread startup.

            _numBlocks = _maxBlocks;
            var stopwatch = Stopwatch.StartNew();
            var processor = new ParallelWorkProcessor<byte[]>(read, process, write, threadCount);
            processor.WaitForFinished(Timeout.Infinite);

            Console.WriteLine("\n\nFinished in " + stopwatch.Elapsed + "\n\n");
        }

        private static byte[] read()
        {
            if (_numBlocks-- == 0)
            {
                return null;
            }

            var result = new byte[128];
            result[0] = (byte)(_maxBlocks-_numBlocks);
            Console.WriteLine("Supplied input: " + result[0]);
            return result;
        }

        private static byte[] process(byte[] data)
        {
            if (data[0] == 10) // Hack for test purposes. Make it REALLY slow for this item!
            {
                Console.WriteLine("Delaying a call to process() for 5s for ID 10");
                Thread.Sleep(5000);
            }

            Thread.Sleep(10 + _rng.Next(50));
            Console.WriteLine("Processed: " + data[0]);
            return data;
        }

        private static void write(byte[] data)
        {
            Console.WriteLine("Received output: " + data[0]);
        }

        private static Random _rng;
        private static int _numBlocks;
        private static int _maxBlocks;
    }
}