正确的实现资源池的方法

时间:2015-04-07 21:04:58

标签: c# multithreading pool

我试图实现管理资源池的东西,以便调用代码可以请求一个对象,并且如果它可用,将从池中获得一个对象,否则它将被用于等待。但是,我无法让同步正常工作。我在我的池类中的内容是这样的(autoEvent最初设置为AutoResetEvent信号:

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。< / p>

问题似乎在于我管理池的大小。容量为5时,我最终会使用6 Foo s。我可以在调试行中看到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;
        }
    }
} 

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

2 个答案:

答案 0 :(得分:2)

您正在做的事情存在一些问题,但您的具体竞争状况可能是由以下情况引起的。想象一下,你有一个容量。

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

2)线程#1抓住它并发出信号。

3)线程#2找不到可用事件并进入容量块。 尚未添加该项目。

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

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

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

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

池中现在有两个容量为1的项目。

但是,您的实施还存在其他潜在问题。

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

答案 1 :(得分:2)

正如评论中已经提到的,计数信号量是你的朋友。 将它与并发堆栈相结合,您就可以获得一个简单的线程安全实现,您仍然可以懒惰地分配池项目。

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

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

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