由于几个未知数,这个问题比预期的要棘手一些:池中资源的行为,对象的预期/所需生存期,需要池的真正原因等。通常,池是专用的-线程池,连接池等-因为当您确切地知道资源的作用并且更重要的是可以控制该资源的实现方式时,更容易进行优化。
既然不是那么简单,我想做的就是提供一种相当灵活的方法,您可以尝试一下,看看哪种方法效果最好。 长期道歉是预先准备的,但是在实现体面的通用资源池方面有很多基础。我真的只是在摸摸表面。
通用池必须具有一些主要的“设置”,包括:
- 资源加载策略-渴望或懒惰;
- 资源加载机制 -如何实际构建一个;
- 访问策略-您提到的“循环”并不是听起来那么简单。此实现可以使用类似但不完美的循环缓冲区,因为该池无法控制何时实际回收资源。其他选项是FIFO和LIFO。FIFO将具有更多的随机访问模式,但是LIFO使实施最近最少使用的释放策略(您说过这超出了范围,但仍然值得一提)大大简化了。
对于资源加载机制,.NET已经给了我们一个干净的抽象-委托。
private Func<Pool<T>, T> factory;
通过池的构造函数,我们就可以完成了。使用具有new()
约束的泛型类型也可以工作,但这更加灵活。
在其他两个参数中,访问策略是更复杂的野兽,因此我的方法是使用基于继承(接口)的方法:
public class Pool<T> : IDisposable
{
// Other code - we'll come back to this
interface IItemStore
{
T Fetch();
void Store(T item);
int Count { get; }
}
}
这里的概念很简单-我们将让公共Pool
类处理诸如线程安全性之类的常见问题,但是为每种访问模式使用不同的“项目存储”。LIFO可以很容易地由堆栈表示,FIFO是一个队列,并且我使用了一个不是很优化但可能足够的循环缓冲区实现,该实现使用List<T>
和指针来近似循环访问模式。
下面的所有类都是-的内部类,Pool<T>
这是一种样式选择,但是由于这些类实际上并不是要在外部使用的Pool
,因此这是最有意义的。
class QueueStore : Queue<T>, IItemStore
{
public QueueStore(int capacity) : base(capacity)
{
}
public T Fetch()
{
return Dequeue();
}
public void Store(T item)
{
Enqueue(item);
}
}
class StackStore : Stack<T>, IItemStore
{
public StackStore(int capacity) : base(capacity)
{
}
public T Fetch()
{
return Pop();
}
public void Store(T item)
{
Push(item);
}
}
这些是显而易见的-堆栈和队列。我认为他们并不需要太多解释。循环缓冲区稍微复杂一点:
class CircularStore : IItemStore
{
private List<Slot> slots;
private int freeSlotCount;
private int position = -1;
public CircularStore(int capacity)
{
slots = new List<Slot>(capacity);
}
public T Fetch()
{
if (Count == 0)
throw new InvalidOperationException("The buffer is empty.");
int startPosition = position;
do
{
Advance();
Slot slot = slots[position];
if (!slot.IsInUse)
{
slot.IsInUse = true;
--freeSlotCount;
return slot.Item;
}
} while (startPosition != position);
throw new InvalidOperationException("No free slots.");
}
public void Store(T item)
{
Slot slot = slots.Find(s => object.Equals(s.Item, item));
if (slot == null)
{
slot = new Slot(item);
slots.Add(slot);
}
slot.IsInUse = false;
++freeSlotCount;
}
public int Count
{
get { return freeSlotCount; }
}
private void Advance()
{
position = (position + 1) % slots.Count;
}
class Slot
{
public Slot(T item)
{
this.Item = item;
}
public T Item { get; private set; }
public bool IsInUse { get; set; }
}
}
我可以选择许多不同的方法,但最重要的是,应该按照创建资源的顺序来访问资源,这意味着我们必须维护对它们的引用,但将其标记为“正在使用”(或不标记为“正在使用”) )。在最坏的情况下,只有一个插槽可用,并且每次获取都需要对缓冲区进行完整的迭代。如果您有成百上千个资源池,并且每秒要获取并释放它们几次,那将是很糟糕的。对于5到10个项目的池来说,这并不是一个真正的问题,在典型情况下,资源很少使用,它只需要提前一个或两个插槽即可。
请记住,这些类是私有内部类-这就是为什么它们不需要大量错误检查的原因,池本身限制了对它们的访问。
抛出一个枚举和一个工厂方法,我们就完成了这一部分:
// Outside the pool
public enum AccessMode { FIFO, LIFO, Circular };
private IItemStore itemStore;
// Inside the Pool
private IItemStore CreateItemStore(AccessMode mode, int capacity)
{
switch (mode)
{
case AccessMode.FIFO:
return new QueueStore(capacity);
case AccessMode.LIFO:
return new StackStore(capacity);
default:
Debug.Assert(mode == AccessMode.Circular,
"Invalid AccessMode in CreateItemStore");
return new CircularStore(capacity);
}
}
下一个要解决的问题是加载策略。我定义了三种类型:
public enum LoadingMode { Eager, Lazy, LazyExpanding };
前两个应该不言自明;第三个是混合的,它延迟加载资源,但直到池满时才真正开始重用任何资源。如果您希望池已满(听起来像您这样做),但想将实际创建它们的开销推迟到首次访问之前(例如,缩短启动时间),那么这将是一个很好的权衡。
既然我们有了item-store抽象,那么加载方法实际上并不太复杂:
private int size;
private int count;
private T AcquireEager()
{
lock (itemStore)
{
return itemStore.Fetch();
}
}
private T AcquireLazy()
{
lock (itemStore)
{
if (itemStore.Count > 0)
{
return itemStore.Fetch();
}
}
Interlocked.Increment(ref count);
return factory(this);
}
private T AcquireLazyExpanding()
{
bool shouldExpand = false;
if (count < size)
{
int newCount = Interlocked.Increment(ref count);
if (newCount <= size)
{
shouldExpand = true;
}
else
{
// Another thread took the last spot - use the store instead
Interlocked.Decrement(ref count);
}
}
if (shouldExpand)
{
return factory(this);
}
else
{
lock (itemStore)
{
return itemStore.Fetch();
}
}
}
private void PreloadItems()
{
for (int i = 0; i < size; i++)
{
T item = factory(this);
itemStore.Store(item);
}
count = size;
}
上面的size
和count
字段分别表示池的最大大小和池所拥有的资源总数(但不一定可用)。 AcquireEager
最简单,它假定商店中已经有商品-这些商品将在施工时即在PreloadItems
最后显示的方法中预先装载。
AcquireLazy
检查池中是否有空闲物品,如果没有,它会创建一个新物品。 AcquireLazyExpanding
只要池尚未达到其目标大小,它将创建一个新资源。我已尝试优化此方法以最大程度地减少锁定,并且希望我没有犯任何错误(我已在多线程条件下对此进行了测试,但显然没有穷举)。
您可能想知道为什么这些方法都不费心检查存储是否已达到最大大小。一会儿,我会解决。
现在为游泳池本身。这是完整的私有数据集,其中一些已经显示:
private bool isDisposed;
private Func<Pool<T>, T> factory;
private LoadingMode loadingMode;
private IItemStore itemStore;
private int size;
private int count;
private Semaphore sync;
回答我在上一段中提到的问题-如何确保我们限制创建的资源总数-事实证明.NET已经为此提供了一个非常好的工具,称为Semaphore,它专门用于允许固定访问资源的线程数(在这种情况下,“资源”是内部项目存储)。由于我们没有实施完整的生产者/消费者队列,因此完全可以满足我们的需求。
构造函数如下所示:
public Pool(int size, Func<Pool<T>, T> factory,
LoadingMode loadingMode, AccessMode accessMode)
{
if (size <= 0)
throw new ArgumentOutOfRangeException("size", size,
"Argument 'size' must be greater than zero.");
if (factory == null)
throw new ArgumentNullException("factory");
this.size = size;
this.factory = factory;
sync = new Semaphore(size, size);
this.loadingMode = loadingMode;
this.itemStore = CreateItemStore(accessMode, size);
if (loadingMode == LoadingMode.Eager)
{
PreloadItems();
}
}
这里应该不足为奇。唯一需要注意的是使用PreloadItems
前面已经显示的方法进行特殊加载的特殊外壳。
既然到目前为止几乎所有内容都已经被抽象地提取出来了,所以实际的Acquire
和Release
方法确实非常简单:
public T Acquire()
{
sync.WaitOne();
switch (loadingMode)
{
case LoadingMode.Eager:
return AcquireEager();
case LoadingMode.Lazy:
return AcquireLazy();
default:
Debug.Assert(loadingMode == LoadingMode.LazyExpanding,
"Unknown LoadingMode encountered in Acquire method.");
return AcquireLazyExpanding();
}
}
public void Release(T item)
{
lock (itemStore)
{
itemStore.Store(item);
}
sync.Release();
}
如前所述,我们使用Semaphore
来控制并发性,而不是认真检查项目存储的状态。只要正确地释放了获得的物品,就不用担心了。
最后但并非最不重要的一点是清理:
public void Dispose()
{
if (isDisposed)
{
return;
}
isDisposed = true;
if (typeof(IDisposable).IsAssignableFrom(typeof(T)))
{
lock (itemStore)
{
while (itemStore.Count > 0)
{
IDisposable disposable = (IDisposable)itemStore.Fetch();
disposable.Dispose();
}
}
}
sync.Close();
}
public bool IsDisposed
{
get { return isDisposed; }
}
该IsDisposed
属性的目的将很快变得清晰。主要Dispose
方法真正要做的就是处置实际的合并项(如果实现)IDisposable
。
现在,您基本上可以按原样使用带try-finally
块的代码了,但是我不喜欢这种语法,因为如果您开始在类和方法之间传递池化的资源,那么它将变得非常混乱。使用资源的主类甚至可能没有对池的引用。它确实变得非常混乱,因此更好的方法是创建一个“智能”池对象。
假设我们从以下简单的接口/类开始:
public interface IFoo : IDisposable
{
void Test();
}
public class Foo : IFoo
{
private static int count = 0;
private int num;
public Foo()
{
num = Interlocked.Increment(ref count);
}
public void Dispose()
{
Console.WriteLine("Goodbye from Foo #{0}", num);
}
public void Test()
{
Console.WriteLine("Hello from Foo #{0}", num);
}
}
这是我们假装的一次性Foo
资源,该资源实现IFoo
并具有一些用于生成唯一身份的样板代码。我们要做的是创建另一个特殊的池对象:
public class PooledFoo : IFoo
{
private Foo internalFoo;
private Pool<IFoo> pool;
public PooledFoo(Pool<IFoo> pool)
{
if (pool == null)
throw new ArgumentNullException("pool");
this.pool = pool;
this.internalFoo = new Foo();
}
public void Dispose()
{
if (pool.IsDisposed)
{
internalFoo.Dispose();
}
else
{
pool.Release(this);
}
}
public void Test()
{
internalFoo.Test();
}
}
这只是将所有“真实”方法代理到其内部IFoo
(我们可以使用诸如Castle之类的动态代理库来做到这一点,但我不会理解这一点)。它还维护对Pool
创建它的的引用,以便当我们创建Dispose
该对象时,它会自动将其释放回池中。 除非已经处置了池,否则这意味着我们处于“清理”模式,在这种情况下,它实际上是在清理内部资源。
使用上面的方法,我们可以编写如下代码:
// Create the pool early
Pool<IFoo> pool = new Pool<IFoo>(PoolSize, p => new PooledFoo(p),
LoadingMode.Lazy, AccessMode.Circular);
// Sometime later on...
using (IFoo foo = pool.Acquire())
{
foo.Test();
}
这是一件非常好的事。这就是说,该代码使用的IFoo
(而不是其创建它的代码)实际上并不需要知道池。您甚至可以使用自己喜欢的DI库并作为提供者/工厂来注入 IFoo
对象Pool<T>
。
我已将完整的代码放在PasteBin上,以供您复制和粘贴之用。还有一个简短的测试程序,您可以使用它来处理不同的加载/访问模式和多线程条件,以使自己确信它是线程安全的,而不是错误的。
如果您对此有任何疑问或担忧,请告诉我。