C # 개체 풀링 패턴 구현


165

누구나 SQL 연결 풀링을 통해 제한된 리소스에 대한 공유 객체 풀 전략을 구현하는 데 유용한 리소스가 있습니까? (즉, 스레드로부터 안전하도록 완전히 구현되어야 함).

설명을위한 @Aaronaught 요청과 관련하여 후속 조치를 수행하려면 풀 사용은 외부 서비스에 대한 요청을로드 밸런싱하는 것입니다. 내가 직접 배치하는 것과는 반대로 즉시 이해하기 쉬운 시나리오에이를 수 있습니다. ISessionNHibernate 의 객체 와 유사하게 작동하는 세션 객체가 있습니다 . 각 고유 세션은 데이터베이스에 대한 연결을 관리합니다. 현재 1 개의 장시간 실행되는 세션 객체가 있으며 서비스 제공 업체 가이 개별 세션의 사용을 제한하는 문제가 발생합니다.

단일 세션이 장기 실행 서비스 계정으로 취급 될 것이라는 기대치가 부족하여 서비스를 망치는 클라이언트로 간주합니다. 여기에 내 질문에 하나의 개별 세션이있는 대신 다른 세션 풀을 만들고 이전에했던 것처럼 단일 초점을 만드는 대신 여러 세션에서 서비스로 요청을 분할합니다.

그 배경이 가치를 제공하지만 일부 질문에 직접 대답하기를 바랍니다.

Q : 개체를 만드는 데 비용이 많이 듭니까?
A : 제한된 리소스 풀이있는 개체가 없습니다 .

Q : 매우 자주 획득 / 출시됩니까?
A : 그렇습니다. 다시 한번 그들은 매 페이지 요청이있을 때마다 1을 획득하고 공개하는 NHibernate ISessions를 생각할 수 있습니다.

Q : 간단한 선착순으로 충분합니까, 아니면 기아를 막을 수있는보다 지능적인 것이 필요합니까?
A : 단순한 라운드 로빈 유형 배포로 충분할 것입니다. 기아로 인해 발신자가 릴리스를 기다리는 동안 사용 가능한 세션이없는 경우를 의미한다고 가정합니다. 다른 발신자가 세션을 공유 할 수 있으므로 실제로 적용 할 수 없습니다. 내 목표는 단일 세션이 아닌 여러 세션에 사용량을 분산시키는 것입니다.

나는 이것이 아마도 객체 풀을 정상적으로 사용하는 것과의 차이라고 생각합니다. 그래서 원래이 부분을 생략하고 기아 상황이 발생하는 것과 반대로 객체 공유를 허용하도록 패턴을 조정하려고 계획 한 이유입니다.

Q : 우선 순위, 게으른 대 열렬한로드 등과 같은 것은 어떻습니까?
A : 우선 순위는 없습니다. 단순화를 위해 풀 자체를 만들 때 사용 가능한 개체 풀을 만들겠다고 가정하기 만하면됩니다.


1
귀하의 요구 사항에 대해 조금 말씀해 주시겠습니까? 모든 풀이 동일한 것은 아닙니다. 개체를 만드는 데 비용이 많이 듭니까? 그것들은 매우 자주 입수 / 출시됩니까? 단순한 선착순으로 충분하거나 더 굶주림을 예방하는보다 지능적인 것이 필요합니까? 우선 순위, 게으른 대 열렬한로드 등과 같은 것은 어떻습니까? 당신이 추가 할 수있는 것은 우리 (또는 적어도 나)가 더 철저한 답변을 얻는 데 도움이 될 것입니다.
Aaronaught

Chris-두 번째와 세 번째 단락을보고이 세션이 실제로 무기한으로 유지되어야하는지 궁금하십니까? 그것은 당신의 서비스 제공 업체가 좋아하지 않는 것 같습니다 (장기 실행 세션). 따라서 필요에 따라 새 세션을 가동시키고 사용하지 않을 때 (일부 지정된 기간이 지난 후) 종료하는 풀 구현을 찾고 있습니다 . 이 작업을 수행 할 수 있지만 조금 더 복잡하므로 확인하고 싶습니다.
Aaronaught

내 솔루션이 단순한 가상이기 때문에 강력한 솔루션이 필요한지 또는 아직 확실하지 않은지 확실하지 않습니다. 내 서비스 제공 업체가 나에게 거짓말을하고 있고 서비스가 과도하게 판매되어 사용자를 탓할 방법에 대한 변명을 발견했을 수도 있습니다.
Chris Marisic

1
TPL DataFlow BufferBlock은 필요한 대부분의 기능을 수행한다고 생각합니다.
쓰셨

1
스레드 환경에서의 풀링은 반복되는 문제이며 리소스 풀 및 리소스 캐시와 같은 디자인 패턴으로 해결됩니다. 체크 아웃 패턴 지향 소프트웨어 아키텍처, 제 3 권 : 자원 관리에 대한 패턴 추가 정보를 원하시면.
Fuhrmanator 2016 년

답변:


59

.NET Core의 개체 풀링

DOTNET 코어 기본 클래스 라이브러리 (BCL)에 첨가 개체 풀링의 구현을 갖는다. 여기 에서 원래 GitHub 문제를 읽고 System.Buffers 코드를 볼 수 있습니다 . 현재 ArrayPool유일하게 사용 가능한 유형이며 어레이를 풀링하는 데 사용됩니다. 여기에 좋은 블로그 게시물이 있습니다 .

namespace System.Buffers
{
    public abstract class ArrayPool<T>
    {
        public static ArrayPool<T> Shared { get; internal set; }

        public static ArrayPool<T> Create(int maxBufferSize = <number>, int numberOfBuffers = <number>);

        public T[] Rent(int size);

        public T[] Enlarge(T[] buffer, int newSize, bool clearBuffer = false);

        public void Return(T[] buffer, bool clearBuffer = false);
    }
}

사용 예는 ASP.NET Core에서 확인할 수 있습니다. 닷넷 코어 BCL에 있기 때문에 ASP.NET 코어는 Newtonsoft.Json의 JSON 시리얼 라이저와 같은 다른 객체와 객체 풀을 공유 할 수 있습니다. 당신이 읽을 수있는 Newtonsoft.Json이 일을하는 방법에 대한 자세한 내용은 블로그 게시물을.

Microsoft Roslyn C # 컴파일러의 개체 풀링

새로운 Microsoft Roslyn C # 컴파일러에는 ObjectPool 유형이 포함되어 있습니다. ObjectPool 유형은 일반적으로 자주 사용되며 가비지 수집이 자주 발생하는 자주 사용하는 객체를 풀링하는 데 사용됩니다. 이렇게하면 발생해야하는 가비지 수집 작업의 양과 크기가 줄어 듭니다. ObjectPool을 사용하는 몇 가지 다른 하위 구현 이 있습니다 (Roslyn에 Object Pooling 구현이 많은 이유는 무엇입니까? 참조). ).

1- SharedPools -BigDefault가 사용되는 경우 20 개 오브젝트 또는 100 개 풀을 저장합니다.

// Example 1 - In a using statement, so the object gets freed at the end.
using (PooledObject<Foo> pooledObject = SharedPools.Default<List<Foo>>().GetPooledObject())
{
    // Do something with pooledObject.Object
}

// Example 2 - No using statement so you need to be sure no exceptions are not thrown.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
// Do something with list
SharedPools.Default<List<Foo>>().Free(list);

// Example 3 - I have also seen this variation of the above pattern, which ends up the same as Example 1, except Example 1 seems to create a new instance of the IDisposable [PooledObject<T>][4] object. This is probably the preferred option if you want fewer GC's.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
try
{
    // Do something with list
}
finally
{
    SharedPools.Default<List<Foo>>().Free(list);
}

2- ListPoolStringBuilderPool- 구현을 엄격하게 분리하지는 않지만 List 및 StringBuilder에 대해 위에 표시된 SharedPools 구현 주위의 래퍼입니다. 따라서 이것은 SharedPools에 저장된 개체 풀을 재사용합니다.

// Example 1 - No using statement so you need to be sure no exceptions are thrown.
StringBuilder stringBuilder= StringBuilderPool.Allocate();
// Do something with stringBuilder
StringBuilderPool.Free(stringBuilder);

// Example 2 - Safer version of Example 1.
StringBuilder stringBuilder= StringBuilderPool.Allocate();
try
{
    // Do something with stringBuilder
}
finally
{
    StringBuilderPool.Free(stringBuilder);
}

3- PooledDictionaryPooledHashSet -ObjectPool을 직접 사용하며 완전히 별개의 개체 풀이 있습니다. 128 개의 개체 풀을 저장합니다.

// Example 1
PooledHashSet<Foo> hashSet = PooledHashSet<Foo>.GetInstance()
// Do something with hashSet.
hashSet.Free();

// Example 2 - Safer version of Example 1.
PooledHashSet<Foo> hashSet = PooledHashSet<Foo>.GetInstance()
try
{
    // Do something with hashSet.
}
finally
{
    hashSet.Free();
}

Microsoft.IO.RecyclableMemoryStream

이 라이브러리는 MemoryStream객체 풀링을 제공 합니다. 의 대체품입니다 System.IO.MemoryStream. 그것은 정확히 같은 의미론을 가지고 있습니다. Bing 엔지니어가 설계했습니다. 블로그 게시물을 읽어 여기 거나 코드를 참조 GitHub의를 .

var sourceBuffer = new byte[]{0,1,2,3,4,5,6,7}; 
var manager = new RecyclableMemoryStreamManager(); 
using (var stream = manager.GetStream()) 
{ 
    stream.Write(sourceBuffer, 0, sourceBuffer.Length); 
}

참고 RecyclableMemoryStreamManager가 전체 살 것이고, 한 번 선언해야합니다 과정은-이 풀이다. 원하는 경우 여러 풀을 사용하는 것이 좋습니다.


2
이것은 좋은 대답입니다. C # 6 & VS2015가 RTM이 된 후 Rosyln 자체에서 사용하도록 튜닝하면 분명히 가장 좋은 답변이 될 것입니다.
Chris Marisic 2016 년

동의하지만 어떤 구현을 사용 하시겠습니까? Roslyn은 3 개를 포함합니다. 답변에서 내 질문에 대한 링크를 참조하십시오.
Muhammad Rehan Saeed 2016 년

1
각각이 매우 명확하게 정의 된 목적을 가지고있는 것처럼 보입니다. 개방형 단일 크기의 선택 모든 신발에 적합합니다.
Chris Marisic 2016 년

1
ArrayPool와 @MuhammadRehanSaeed 큰 도움
크리스 Marisic

1
이것이 RecyclableMemoryStream바로 초 고성능 최적화를위한 놀라운 추가 사항입니다.
Chris Marisic

315

풀링되는 자원의 동작, 개체의 예상 / 필수 수명, 풀이 필요한 실제 이유 등 일반적으로 풀은 특수 목적의 스레드입니다. 풀, 연결 풀 등-리소스의 기능을 정확히 알고 제어 할 수있는 리소스를 정확히 파악할 때 최적화하기가 더 쉽기 때문에 리소스의 구현 방법을 하는 .

그렇게 간단하지 않기 때문에 내가 시도한 것은 실험하고 가장 잘 작동하는 것을 볼 수있는 상당히 유연한 접근 방식을 제공하는 것입니다. 긴 게시물에 대해 사전에 사과하지만 적절한 범용 리소스 풀을 구현할 때 다루어야 할 근거가 많이 있습니다. 그리고 난 정말 표면을 긁적입니다.

범용 풀에는 다음을 포함하여 몇 가지 주요 "설정"이 있어야합니다.

  • 자원 로딩 전략-간절하거나 게으른;
  • 자원 로딩 메커니즘 -실제로 구성하는 방법;
  • 접근 전략-당신은 들리는 것처럼 간단하지 않은 "라운드 로빈"을 언급합니다. 이 구현은 풀이 리소스가 실제로 재생되는시기를 제어 할 수 없으므로 유사 하지만 완벽하지 않은 순환 버퍼를 사용할 수 있습니다 . 다른 옵션은 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;
    }

위 의 sizecount필드는 풀의 최대 크기와 풀이 소유 한 총 리소스 수를 나타냅니다 (그러나 반드시 사용 가능한 것은 아님 ). 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 이미 앞에서 설명한 방법을 .

지금까지 거의 모든 것이 깨끗하게 요약되었으므로 실제 방법 AcquireRelease방법은 매우 간단합니다.

    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 라이브러리와 공급자 / 공장을 사용하여 개체를 주입 할 수도 있습니다 .IFooPool<T>


나는 넣었습니다 페이스트 빈에 전체 코드를 하여 복사 및 붙여 넣기 즐거움을 위해. 스레드가 안전하고 버그가 없다는 것을 만족시키기 위해 다양한 로딩 / 액세스 모드와 멀티 스레드 조건을 가지고 놀 수 있는 간단한 테스트 프로그램도 있습니다.

이에 대해 궁금한 점이 있으면 알려주세요.


62
내가 읽은 가장 완전하고 도움이되며 흥미로운 답변 중 하나입니다.
Josh Smeaton

이 응답에 대해 @Josh에 더 동의 할 수 없었습니다. 특히 PooledFoo 부분에서는 객체를 릴리스하는 것이 항상 매우 누출 된 방식으로 처리되어 사용을 사용하는 것이 가장 합리적이라고 생각했습니다. 당신이 보여준 것처럼 나는 단지 앉아서 앉아서 당신의 대답이 나에게 문제를 해결하는 데 필요한 모든 정보를 제공하는 곳에 그것을 구축하려고 노력했다. 내 특정 상황에서는 스레드간에 인스턴스를 공유하고 풀로 다시 해제 할 필요가 없기 때문에 대부분 이것을 단순화 할 수 있다고 생각합니다.
Chris Marisic

그러나 간단한 접근 방식이 먼저 작동하지 않으면 필자의 사례에서 지능적으로 릴리스를 처리하는 방법에 대한 몇 가지 아이디어가 있습니다. 가장 구체적으로 세션 자체에 오류가 있음을 확인하고이를 폐기하고 풀로 새 세션을 교체 할 수 있도록 릴리스를 설정한다고 생각합니다. 이 시점 의이 게시물이 C # 3.0의 객체 풀링에 대한 결정적인 가이드 인 것에 관계없이 다른 사람이 이것에 대해 더 많은 의견을 가지고 있는지 기대하고 있습니다.
Chris Marisic

@Chris : WCF 클라이언트 프록시에 대해 이야기하고 있다면 효과적으로 사용할 수있는 의존성 인젝터 또는 메소드 인터셉터가 필요하지만 그 패턴도 있습니다. DI 버전은 커스터마이즈 프로 바이더와 함께 커널을 사용하여 새로운 결함이있는 버전을 얻습니다. 방법 차단 버전 (기본 설정)은 기존 프록시를 래핑하고 각각에 결함 검사를 삽입합니다. 나는 이것을 풀에 통합하는 것이 얼마나 쉬운 지 잘 모르겠습니다 (방금 쓴 적이 있기 때문에 실제로 시도하지는 않았습니다!). 그러나 가능할 것입니다.
Aaronaught

5
대부분의 상황에서 약간 과장되어 있지만 매우 인상적입니다. 나는 이와 같은 것이 프레임 워크의 일부가 될 것으로 기대합니다.
ChaosPandion

7

이와 같은 것이 귀하의 요구에 적합 할 수 있습니다.

/// <summary>
/// Represents a pool of objects with a size limit.
/// </summary>
/// <typeparam name="T">The type of object in the pool.</typeparam>
public sealed class ObjectPool<T> : IDisposable
    where T : new()
{
    private readonly int size;
    private readonly object locker;
    private readonly Queue<T> queue;
    private int count;


    /// <summary>
    /// Initializes a new instance of the ObjectPool class.
    /// </summary>
    /// <param name="size">The size of the object pool.</param>
    public ObjectPool(int size)
    {
        if (size <= 0)
        {
            const string message = "The size of the pool must be greater than zero.";
            throw new ArgumentOutOfRangeException("size", size, message);
        }

        this.size = size;
        locker = new object();
        queue = new Queue<T>();
    }


    /// <summary>
    /// Retrieves an item from the pool. 
    /// </summary>
    /// <returns>The item retrieved from the pool.</returns>
    public T Get()
    {
        lock (locker)
        {
            if (queue.Count > 0)
            {
                return queue.Dequeue();
            }

            count++;
            return new T();
        }
    }

    /// <summary>
    /// Places an item in the pool.
    /// </summary>
    /// <param name="item">The item to place to the pool.</param>
    public void Put(T item)
    {
        lock (locker)
        {
            if (count < size)
            {
                queue.Enqueue(item);
            }
            else
            {
                using (item as IDisposable)
                {
                    count--;
                }
            }
        }
    }

    /// <summary>
    /// Disposes of items in the pool that implement IDisposable.
    /// </summary>
    public void Dispose()
    {
        lock (locker)
        {
            count = 0;
            while (queue.Count > 0)
            {
                using (queue.Dequeue() as IDisposable)
                {

                }
            }
        }
    }
}

사용법 예

public class ThisObject
{
    private readonly ObjectPool<That> pool = new ObjectPool<That>(100);

    public void ThisMethod()
    {
        var that = pool.Get();

        try
        { 
            // Use that ....
        }
        finally
        {
            pool.Put(that);
        }
    }
}

1
그 이전 의견을 긁어 라. 이 풀에는 임계 값이없는 것 같고 필요하지 않을 수도 있기 때문에 이상하다고 생각했습니다. 요구 사항에 따라 다릅니다.
Aaronaught

1
@Aaronaught-정말 이상합니까? 필요한 기능 만 제공하는 경량 풀을 만들고 싶었습니다. 클래스를 올바르게 사용하는 것은 클라이언트의 책임입니다.
ChaosPandion

1
백킹 유형을 List / HashTable 등으로 변경하고 롤오버하도록 카운터를 변경하여 내 목적에 맞게 조정할 수있는 매우 간단한 솔루션의 경우 +1입니다. 무작위 질문 풀 객체 자체의 관리를 어떻게 처리합니까? 싱글 톤으로 정의하는 IOC 컨테이너에 넣었습니까?
Chris Marisic

1
정적 읽기 전용이어야합니까? 그러나 나는 당신이 finally 문 안에 넣을 것이 이상하다고 생각합니다. 예외가 있다면 그것이 자기 자신의 객체에 결함이있을 가능성이 없을까요? Put메소드 내에서 처리 하고 객체에 결함이 있는지 여부를 확인하고 이전 인스턴스를 삽입하는 대신 풀에 추가 할 새 인스턴스를 작성하기 위해 단순화 하시겠습니까?
Chris Marisic

1
@Chris-과거에 유용한 간단한 도구를 제공하고 있습니다. 나머지는 당신에게 달려 있습니다. 적합하다고 생각되는대로 코드를 수정하고 사용하십시오.
ChaosPandion

6

그 링크 주셔서 감사합니다. 이 구현에는 크기 제한이 없으므로 개체 생성에 급증이 발생하면 해당 급증이 수집되지 않으며 다른 급증이 발생할 때까지 사용되지 않을 것입니다. 그것은 매우 간단하고 이해하기 쉽고 최대 크기 제한을 추가하는 것은 어렵지 않습니다.
무하마드 Rehan Saeed

니스와 간단한
다니엘 드 ZWAAN

4

당시 Microsoft는 COM 개체에 대한 개체 풀링을 수행하기 위해 Microsoft Transaction Server (MTS) 이상 COM +를 통해 프레임 워크를 제공했습니다. 이 기능은 .NET Framework 및 현재 Windows Communication Foundation의 System.EnterpriseServices로 전달되었습니다.

WCF의 개체 풀링

이 문서는 .NET 1.1에서 작성되었지만 WCF가 선호되는 방법 임에도 불구하고 현재 버전의 Framework에 적용되어야합니다.

개체 풀링 .NET


IInstanceProvider내 솔루션에 이것을 구현할 때 인터페이스가 있음을 보여주기 위해 +1 . 필자는 항상 적합한 정의를 제공 할 때 Microsoft 제공 인터페이스 뒤에 코드를 쌓아 놓는 팬입니다.
Chris Marisic

4

저는 Aronaught의 구현을 정말 좋아합니다. 특히 그는 세마포어를 사용하여 리소스를 사용할 수있게되기를 기다립니다. 내가 추가하고 싶은 몇 가지 추가 사항이 있습니다.

  1. 변경 sync.WaitOne()sync.WaitOne(timeout)와에 매개 변수로 타임 아웃을 노출Acquire(int timeout) 방법. 또한 스레드가 객체를 사용할 수 있기를 기다리는 시간이 초과 될 때 조건을 처리해야합니다.
  2. Recycle(T item)예를 들어, 장애가 발생했을 때 객체를 재활용해야하는 상황을 처리하는 메소드를 추가하십시오 .

3

이것은 풀에 제한된 수의 개체가있는 또 다른 구현입니다.

public class ObjectPool<T>
    where T : class
{
    private readonly int maxSize;
    private Func<T> constructor;
    private int currentSize;
    private Queue<T> pool;
    private AutoResetEvent poolReleasedEvent;

    public ObjectPool(int maxSize, Func<T> constructor)
    {
        this.maxSize = maxSize;
        this.constructor = constructor;
        this.currentSize = 0;
        this.pool = new Queue<T>();
        this.poolReleasedEvent = new AutoResetEvent(false);
    }

    public T GetFromPool()
    {
        T item = null;
        do
        {
            lock (this)
            {
                if (this.pool.Count == 0)
                {
                    if (this.currentSize < this.maxSize)
                    {
                        item = this.constructor();
                        this.currentSize++;
                    }
                }
                else
                {
                    item = this.pool.Dequeue();
                }
            }

            if (null == item)
            {
                this.poolReleasedEvent.WaitOne();
            }
        }
        while (null == item);
        return item;
    }

    public void ReturnToPool(T item)
    {
        lock (this)
        {
            this.pool.Enqueue(item);
            this.poolReleasedEvent.Set();
        }
    }
}



당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.