有效地等待一个或多个资源变得可用

时间:2013-02-24 14:47:05

标签: c# multithreading task-parallel-library multitasking

在我花了太长时间重新发明轮子之前,我想检查一下.Net中已经有一个类可以做我想要的。

我想要的东西有点像信号量(或者甚至像CountdownEvent),但略有不同。

我有一个要求,我可以使用不同数量的“资源”,并且我希望线程在没有可用资源时有效地等待。与此同时,另一个线程可以释放资源,该资源应该立即释放另一个等待的线程。

这听起来很像信号量,但它不是因为信号量(据我所见)将每个线程视为计算它们的“资源”。

无论如何,这是我对我想要的第一个简单实现。它没有处置,代码合同,错误处理,超时支持或取消支持,但它应该证明我想要的东西:

public sealed class ResourceCounter
{
    /// <summary>Create with the specified number of resources initially available.</summary>

    public ResourceCounter(int resourceCount)
    {
        _resourceCount = resourceCount;

        if (_resourceCount > 0)
        {
            _resourceAvailable.Set();
        }
    }

    /// <summary>Acquires a resource. Waits forever if necessary.</summary>

    public void Acquire()
    {
        while (true)
        {
            _resourceAvailable.Wait();

            lock (_lock)
            {
                if (_resourceCount > 0)
                {
                    if (--_resourceCount == 0)
                    {
                        _resourceAvailable.Reset();
                    }

                    return;
                }
            }
        }
    }

    /// <summary>Releases a resource.</summary>

    public void Release()
    {
        lock (_lock)
        {
            ++_resourceCount;
            _resourceAvailable.Set();
        }
    }

    private int _resourceCount;
    private readonly object _lock = new object(); 
    private readonly ManualResetEventSlim _resourceAvailable = new ManualResetEventSlim();
}

使用模式非常简单:

  1. 使用所需的初始资源计数(可以为零或更多)构造一个ResourceCounter。

  2. 想要获取资源的线程调用ResourceCounter.Acquire(),该资源在资源可用且已被获取后才会返回。

  3. 想要释放资源的线程调用ResourceCounter.Release(),它将释放资源并立即返回。

  4. 请注意,任何线程都可以释放资源;它不一定是获得资源的人。

    我正在使用它作为某些多线程管道代码的一部分,其中一个线程负责将工作项排入队列,多个线程正在处理工作项,另一个线程正在输出已处理的工作项。输出已处理工作项的线程必须多路复用它们(因为处理线程可以按任何顺序输出完成的项),并且我需要一种机制来阻止工作项在多路复用器等待迟到的项目时无休止地排队。

    (有关此问题的背景信息,请参阅Pipelines, multiplexing, and unbounded buffering。)

    无论如何,有没有什么可以做到的,或者我应该继续为它开发自己的课程?


    [编辑]

    如下所述,SemaphoreSlim完全正确。我拒绝了它,因为我认为调用Wait()的线程必须是调用Release()的线程,但事实并非如此。这是我在星期天编码的结果......;)

2 个答案:

答案 0 :(得分:3)

使用队列进行通信可以更轻松地构建多级流水线架构。生产者线程将项放入工作队列,一个或多个工作线程出列并处理项,并将它们添加到输出队列。最后一个线程读取输出队列并输出数据。

在.NET中,可以使用BlockingCollection轻松完成。

有关两阶段管道的示例,请参阅https://stackoverflow.com/a/5108487/56778。添加另一个阶段很简单。

为了解决输出线程出现问题的问题,我使用min heap将输出队列作为优先级队列。我的项目由顺序记录号识别,因此输出线程知道下一个要输出的记录号。它将在AutoResetEvent上等待项目放置在队列中(工作进程将在项目入队时设置事件)。然后输出线程将查看顶部项目以查看它是否与预期项目匹配。如果没有,它会再次等待事件。

这非常有效,因为它消除了第二个队列。该块位于输出队列中,属于它。表现非常适合我的目的。将项目排队是O(log n)操作,但实际上n非常小。即使队列中有100,000个项目,与处理记录所花费的时间相比,将项目排队所需的时间也是微不足道的。

您仍然可以使用BlockingCollection。您只需要使二进制堆类实现IProducerConsumerCollection接口。我通过向A Generic BinaryHeap class中发布的简单二进制堆类添加锁来实现这一点。然后,您可以将其中一个提供给BlockingCollection构造函数,如下所示:

BlockingCollection<MyRecord> = 
    new BlockingCollection<ConcurrentBinaryHeap<MyRecord>>(
    new ConcurrentBinaryHeap<MyRecord>, MaxQueueSize);
但是,这里存在潜在的僵局。如果队列填满(即超过初始化BlockingCollection时设置的最大值),则迟到的线程无法将项目入队,所有工作都将完全停止。这在我的实践中从未发生过,因为虽然我的每个记录处理时间不同,但它们并没有发生

如果这是一个问题,您可以增加队列大小(只有在您可以肯定地说您不会填满队列时才有效),或者为下一个预期项目提供备用通道,如果队列已满。我做了那个工作,但就我的目的而言,增加队列大小更容易。

如果您有兴趣,我可以浏览我的档案,找到ConcurrentBinaryHeap课程。

答案 1 :(得分:1)

线程彼此通信的方式与资源或其锁定机制无关。使用临时消息传递系统(消息队列,事件或任何适合您需要的东西),没有什么能阻止您在同一进程中传递信号量和资源。