풀링되는 자원의 동작, 개체의 예상 / 필수 수명, 풀이 필요한 실제 이유 등 일반적으로 풀은 특수 목적의 스레드입니다. 풀, 연결 풀 등-리소스의 기능을 정확히 알고 제어 할 수있는 리소스를 정확히 파악할 때 최적화하기가 더 쉽기 때문에 리소스의 구현 방법을 하는 .
그렇게 간단하지 않기 때문에 내가 시도한 것은 실험하고 가장 잘 작동하는 것을 볼 수있는 상당히 유연한 접근 방식을 제공하는 것입니다. 긴 게시물에 대해 사전에 사과하지만 적절한 범용 리소스 풀을 구현할 때 다루어야 할 근거가 많이 있습니다. 그리고 난 정말 표면을 긁적입니다.
범용 풀에는 다음을 포함하여 몇 가지 주요 "설정"이 있어야합니다.
- 자원 로딩 전략-간절하거나 게으른;
- 자원 로딩 메커니즘 -실제로 구성하는 방법;
- 접근 전략-당신은 들리는 것처럼 간단하지 않은 "라운드 로빈"을 언급합니다. 이 구현은 풀이 리소스가 실제로 재생되는시기를 제어 할 수 없으므로 유사 하지만 완벽하지 않은 순환 버퍼를 사용할 수 있습니다 . 다른 옵션은 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>
라운드 로빈 액세스 패턴을 근사하기 위해 and 및 포인터를 사용하여 최적화되지는 않았지만 아마도 적절한 원형 버퍼 구현을 사용했습니다 .
아래의 모든 클래스는 내부 클래스입니다. 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 };
처음 두 가지는 설명이 필요합니다. 세 번째는 일종의 하이브리드이며, 리소스를 지연로드하지만 풀이 가득 찰 때까지 실제로 리소스를 재사용하기 시작하지 않습니다. 풀을 가득 채우고 싶을 때 (사운드처럼 들리지만) 처음 액세스 할 때까지 (즉, 시작 시간을 향상시키기 위해) 실제로 풀을 만드는 데 드는 비용을 미루고 싶을 경우 이는 절충이됩니다.
로딩 방법은 실제로 너무 복잡하지 않습니다. 이제 아이템 저장소 추상화가 생겼습니다.
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);
}
}
다음 은 고유 한 ID를 생성하기위한 상용구 코드를 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>
나는 넣었습니다 페이스트 빈에 전체 코드를 하여 복사 및 붙여 넣기 즐거움을 위해. 스레드가 안전하고 버그가 없다는 것을 만족시키기 위해 다양한 로딩 / 액세스 모드와 멀티 스레드 조건을 가지고 놀 수 있는 간단한 테스트 프로그램도 있습니다.
이에 대해 궁금한 점이 있으면 알려주세요.