【问题标题】:Correct way to implement a resource pool实现资源池的正确方法
【发布时间】:2015-04-07 21:04:58
【问题描述】:

我正在尝试实现一些管理资源池的东西,以便调用代码可以请求一个对象,如果它可用,将从池中获得一个对象,否则它将等待。但是,我无法使同步正常工作。我在泳池课中的内容是这样的(其中autoEventAutoResetEvent,最初设置为信号:

public Foo GetFooFromPool()
{
    autoEvent.WaitOne();
    var foo = Pool.FirstOrDefault(p => !p.InUse);
    if (foo != null)
    {
        foo.InUse = true;
        autoEvent.Set();
        return foo;
    }
    else if (Pool.Count < Capacity)
    {
        System.Diagnostics.Debug.WriteLine("count {0}\t capacity {1}", Pool.Count, Capacity);
        foo = new Foo() { InUse = true };
        Pool.Add(foo);
        autoEvent.Set();
        return foo;
    }
    else
    {
        return GetFooFromPool();
    }
}

public void ReleaseFoo(Foo p)
{
    p.InUse = false;
    autoEvent.Set();
}

这个想法是,当您致电GetFooFromPool 时,您会等到收到信号后,然后尝试找到一个未使用的现有Foo。如果你找到了,我们将其设置为InUse,然后触发一个信号,以便其他线程可以继续。如果没有找到,我们检查池是否已满。如果没有,我们创建一个新的Foo,将其添加到池中并再次发出信号。如果这两个条件都不满足,我们将再次调用GetFooFromPool 再次等待。

现在在ReleaseFoo 中,我们只需将InUse 设置回false,并指示下一个线程在GetFooFromPool(如果有)中等待以尝试获取Foo

问题似乎在于我管理池的大小。容量为5,我最终得到6Foos。我可以在我的调试行中看到count 0 出现了几次,count 1 也可能出现了几次。很明显,我有多个线程进入块,据我所知,它们不应该能够。

我在这里做错了什么?

编辑:像这样的双重检查锁:

else if (Pool.Count < Capacity)
{
    lock(locker)
    {
        if (Pool.Count < Capacity)
        {
            System.Diagnostics.Debug.WriteLine("count {0}\t capacity {1}", Pool.Count, Capacity);
            foo = new Foo() { InUse = true };
            Pool.Add(foo);
            autoEvent.Set();
            return foo;
        }
    }
} 

似乎可以解决问题,但我不确定这是不是最优雅的方法。

【问题讨论】:

  • 嗯,看起来Semaphores 在这种情况下可能会更好地切芥末。这似乎就是专门为处理这种情况而设计的数据结构。
  • ConcurrentQueue 可能会做你想做的一切......
  • 旁注:使用await pool.GetFooFromPoolAsync(...) 可能会更好,而不是同步等待/阻塞你;会得到常规的锁定/事件。
  • 顺便说一句:如果您有一个动态增长的池,那么您还应该有一些代码来缩小它。否则,一个固定大小(足够大)的池就足够了。

标签: c# multithreading pool


【解决方案1】:

正如 cmets 中已经提到的,计数信号量是您的朋友。 将它与并发堆栈结合起来,您就得到了一个非常简单、线程安全的实现,您仍然可以在其中延迟分配池项目。

下面的基本实现提供了这种方法的示例。请注意,这里的另一个优点是您不需要使用 InUse 成员作为标记来“污染”您的池项目来跟踪内容。

请注意,作为微优化,在这种情况下,堆栈优先于队列,因为它将提供池中最近返回的实例,该实例可能仍在例如一级缓存。

public class GenericConcurrentPool<T> : IDisposable where T : class
{
    private readonly SemaphoreSlim _sem;
    private readonly ConcurrentStack<T> _itemsStack;
    private readonly Action<T> _onDisposeItem;
    private readonly Func<T> _factory;

    public GenericConcurrentPool(int capacity, Func<T> factory, Action<T> onDisposeItem = null)
    {
        _itemsStack = new ConcurrentStack<T>(new T[capacity]);
        _factory = factory;
        _onDisposeItem = onDisposeItem;
        _sem = new SemaphoreSlim(capacity);
    }

    public async Task<T> CheckOutAsync()
    {
        await _sem.WaitAsync();
        return Pop();
    }

    public T CheckOut()
    {
        _sem.Wait();
        return Pop();
    }

    public void CheckIn(T item)
    {
        Push(item);
        _sem.Release();
    }

    public void Dispose()
    {
        _sem.Dispose();
        if (_onDisposeItem != null)
        {
            T item;
            while (_itemsStack.TryPop(out item))
            {
                if (item != null)
                    _onDisposeItem(item);
            }
        }
    }

    private T Pop()
    {
        T item;
        var result = _itemsStack.TryPop(out item);
        Debug.Assert(result);
        return item ?? _factory();
    }

    private void Push(T item)
    {
        Debug.Assert(item != null);
        _itemsStack.Push(item);
    }
}

【讨论】:

  • 谢谢。我认为这比我拥有的要好得多。我已经在考虑堆栈或队列会是更好的选择,我很欣赏这里关于如何使信号量工作的解释。
  • 在您的CheckIn 中,_sem.Release() 不应该是函数的 last 行吗?所以在我签入之前,有人不能尝试结帐吗?
【解决方案2】:

您正在执行的操作存在一些问题,但您的特定竞争状况可能是由以下情况引起的。想象一下你有一个容量。

1) 池中有一个未使用的项目。

2) 线程 #1 抓取它并发出事件信号。

3) 线程#2 没有找到可用的事件并进入容量块。 它还没有添加项目。

4) 线程 #1 将项目返回到池中并发出事件信号。

5) 使用另外两个线程(例如 #3、#4)重复步骤 1、2 和 3。

6) 线程 #2 将项目添加到池中。

7) 线程 #4 将项目添加到池中。

现在池中有两个项目,容量为一个。

但是,您的实现还有其他潜在问题。

  • 根据 Pool.Count 和 Add() 的同步方式,您可能看不到最新值。
  • 您可能有多个线程抓取同一个未使用的项目
  • 使用 AutoResetEvent 控制访问会使您遇到难以发现的问题(例如这个问题),因为您尝试使用无锁解决方案,而不是仅仅使用锁定并使用 Monitor.Wait() 和 Monitor.Pulse()这个目的。

【讨论】:

    猜你喜欢
    • 2018-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-03
    • 2010-09-21
    • 1970-01-01
    • 2017-10-17
    相关资源
    最近更新 更多