实现对象池的好方法是什么?

时间:2014-09-03 08:33:53

标签: c# .net task-parallel-library async-await tpl-dataflow

我有第三方课程,我们称之为Analyser。这个类非常擅长分析,但是实例化并且不支持多线程是昂贵的(秒)。

我的应用程序需要提供涉及调用Analyser的请求。这些请求将同时发生。

我想我需要创建一个通用类,比如

public class Pool<T>
{
    public Pool(Func<T> instantiator, int size)
    {
        ...
    }

    public async Task<TResult> Invoke<TResult>(
            Func<T, TResult> target,
            CancellationToken cancellationToken)
    {
        // await the first available T,
        // lock the T,
        // invoke the target, return the result
        // release the lock
    }
}

此类将一般性地封装池功能。

我的问题是,实现这个类的正确方法是什么。它是否已经存在不同的名称?我应该使用TPL.DataFlow吗?我应该亲自动手吗?

被定义为可靠的线程安全,更容易维护更好。


如果通用Pool是解决问题的错误方法,请提供正确的替代方案。


Pool类将使用类似的东西。

private readonly Pool<Analyser> pool = new Pool<Analyser>(
        () => new Analyser(a, b, c),
        100);

public async Task<string> ProcessRequest(
        string raw,
        CancellationToken cancellationToken)
{
    return await this.pool.Invoke(
        analyser => analyser.Analyse(raw),
        cancellationToken);
}

3 个答案:

答案 0 :(得分:3)

我认为构建一个通用池将是一项非常复杂的任务,因此我将会非常有趣: - )

注意:我与您的愿景不同的最重要的一点是,我不希望池处理与其管理的对象相关的线程问题。该池有一些与线程安全相关的代码,但仅用于管理它自己的状态(实例列表)。 线程启动,停止/和/或取消是池的客户端和构造对象的关注点,而不是池本身。

我会先:

  1. 用于由池维护的对象的一次性包装器,在处理时将对象返回池中
  2. 构造或重用可用实例并在将实例返回给客户端之前将其包装的池。
  3. 超简化实施:

    class PoolItem<T> : IDisposable
    {
        public event EventHandler<EventArgs> Disposed;
    
    
        public PoolItem(T wrapped)
        {
            WrappedObject = wrapped;
        }
    
    
        public T WrappedObject { get; private set; }
    
    
        public void Dispose()
        {
            Disposed(this, EventArgs.Empty);
        }
    }
    

    现在是游泳池:

    class Pool<T> where T : class
    {
        private static readonly object m_SyncRoot = new object();
    
        private readonly Func<T> m_FactoryMethod;
        private List<T> m_PoolItems = new List<T>();
    
    
        public Pool(Func<T> factoryMethod)
        {
            m_FactoryMethod = factoryMethod;
        }
    
    
        public PoolItem<T> Get()
        {
            T target = null;
    
            lock (m_SyncRoot)
            {
                if (m_PoolItems.Count > 0)
                {
                    target = m_PoolItems[0];
                    m_PoolItems.RemoveAt(0);
                }
            }
    
            if (target == null)
                target = m_FactoryMethod();
    
            var wrapper = new PoolItem<T>(target);
            wrapper.Disposed += wrapper_Disposed;
    
            return wrapper;
        }
    
    
        void wrapper_Disposed(object sender, EventArgs e)
        {
            var wrapper = sender as PoolItem<T>;
    
            lock (m_SyncRoot)
            {
                m_PoolItems.Add(wrapper.WrappedObject);
            }
        }
    }
    

    用法:

    class ExpensiveConstructionObject
    {
        public ExpensiveConstructionObject()
        {
            Console.WriteLine("Executing the expensive constructor...");
        }
    
        public void Do(string stuff)
        {
            Console.WriteLine("Doing: " + stuff);
        }
    }
    
        class Program
    {
        static void Main(string[] args)
        {
            var pool = new Pool<ExpensiveConstructionObject>(() => new ExpensiveConstructionObject());
    
            var t1 = pool.Get();
            t1.WrappedObject.Do("task 1");
    
            using (var t2 = pool.Get())
                t2.WrappedObject.Do("task 2");
    
            using (var t3 = pool.Get())
                t3.WrappedObject.Do("task 3");
    
            t1.Dispose();
    
            Console.ReadLine();
        }
    }
    

    接下来的步骤是:

    1. 经典泳池功能:初始尺寸,最大尺寸
    2. 动态代理,允许Pool :: Get返回类型为T,而不是PoolItem
    3. 维护包装器列表,如果调用者没有自己处理,则处理它们

答案 1 :(得分:3)

IIUC您尝试实现的是一个通用对象池,当您没有资源可供使用时,您需要异步等待直到您这样做。

最简单的解决方案是使用TPL Dataflow BufferBlock来保存项目,并在空白时等待。在您的API中,您将获得一个委托并运行它,但我建议您从池中返回实际项目,并让用户决定如何处理它:

public class ObjectPool<TItem>
{
    private readonly BufferBlock<TItem> _bufferBlock;
    private readonly int _maxSize;
    private readonly Func<TItem> _creator;
    private readonly CancellationToken _cancellationToken;
    private readonly object _lock;
    private int _currentSize;

    public ObjectPool(int maxSize, Func<TItem> creator, CancellationToken cancellationToken)
    {
        _lock = new object();
        _maxSize = maxSize;
        _currentSize = 1;
        _creator = creator;
        _cancellationToken = cancellationToken;
        _bufferBlock = new BufferBlock<TItem>(new DataflowBlockOptions{CancellationToken = cancellationToken});
    }

    public void Push(TItem item)
    {
        if (!_bufferBlock.Post(item) || _bufferBlock.Count > _maxSize)
        {
            throw new Exception();
        }
    }

    public Task<TItem> PopAsync()
    {
        TItem item;
        if (_bufferBlock.TryReceive(out item))
        {
            return Task.FromResult(item);
        }
        if (_currentSize < _maxSize)
        {
            lock (_lock)
            {
                if (_currentSize < _maxSize)
                {
                    _currentSize++;
                    _bufferBlock.Post(_creator());
                }
            }
        }

        return _bufferBlock.ReceiveAsync();
    }
}

<强>说明:

  • 我使用锁来确保您一次只创建一个新项目,如果需要很长时间,可以使用AsyncLock轻松替换。
  • 我使用Double Check Locking来优化已经创建了所有项目的常见情况。
  • PopAsync返回Task但不是异步方法,因此只要有要返回的项目,它就会同步完成。它仅在池为空且达到限制时才等待。

您可以添加一个返回IDisposable的方法,这样您就可以放心使用scope而无需担心:

public async Task<Disposable> GetDisposableAsync()
{
    return new Disposable(this, await PopAsync());
}

public class Disposable : IDisposable
{
    private readonly ObjectPool<TItem> _pool;
    public TItem Item { get; set; }

    public Disposable(ObjectPool<TItem> pool, TItem item)
    {
        Item = item;
        _pool = pool;
    }
    public void Dispose()
    {
        _pool.Push(Item);
    }
}

答案 2 :(得分:-1)

游泳池是一个很好的解决方案。毕竟,一个池完全用于此目的(维护一组对象,每次实例化都太昂贵了:数据库连接,线程等等。)

但是,如果你想构建一个泛型池,你必须非常小心:你的代码用户可能做“意外”的事情,并最终自己开始射击。

锁定,例如:你应该确实检查这不会导致死锁。如果需要,可以即时扩展池,或者如果代表要求更多对象,则抛出池... 也应该谨慎对待例外情况。

因此,“等待第一个可用的T”和“锁定T”步骤应该由池完全处理,并且应该进行所有必要的检查以避免尴尬的情况。您可以考虑提供您的“客户端代码”(目标)以及对池的引用,以便在需要时提供额外的锁定功能(例如嵌套锁定或类似的东西)

更实际的是:您可以从专门针对Analyser课程的解决方案开始,然后在需要时从那里开始使用通用池?