Thực hiện mô hình nhóm đối tượng C #


165

Có ai có một nguồn lực tốt để thực hiện chiến lược nhóm đối tượng dùng chung cho một nguồn tài nguyên giới hạn trong tĩnh của kết nối Sql không? (tức là sẽ được thực hiện đầy đủ rằng nó là chủ đề an toàn).

Để theo dõi liên quan đến yêu cầu @Aaronaught để làm rõ việc sử dụng nhóm sẽ dành cho các yêu cầu cân bằng tải cho một dịch vụ bên ngoài. Để đặt nó trong một kịch bản có lẽ sẽ dễ hiểu hơn ngay lập tức trái ngược với tình huống trực tiếp của tôi. Tôi có một đối tượng phiên có chức năng tương tự như ISessionđối tượng từ NHibernate. Mỗi phiên duy nhất quản lý kết nối của nó với cơ sở dữ liệu. Hiện tại tôi có 1 đối tượng phiên chạy dài và đang gặp phải sự cố trong đó nhà cung cấp dịch vụ của tôi đang giới hạn tỷ lệ sử dụng phiên cá nhân này.

Do họ không kỳ vọng rằng một phiên duy nhất sẽ được coi là một tài khoản dịch vụ chạy dài, họ dường như coi đó là một khách hàng đang cản trở dịch vụ của họ. Điều này đưa tôi đến câu hỏi của tôi ở đây, thay vì có 1 phiên riêng lẻ, tôi sẽ tạo một nhóm các phiên khác nhau và phân chia các yêu cầu cho dịch vụ qua nhiều phiên đó thay vì tạo một tiêu điểm như tôi đã làm trước đây.

Hy vọng rằng nền tảng cung cấp một số giá trị nhưng để trả lời trực tiếp một số câu hỏi của bạn:

Q: Các đối tượng đắt tiền để tạo ra?
A: Không có đối tượng là một nguồn tài nguyên hạn chế

Q: Liệu chúng có được mua / phát hành rất thường xuyên không?
Trả lời: Có, một lần nữa họ có thể nghĩ về NHibernate ISimes nơi 1 thường được mua và phát hành trong suốt thời gian của mỗi yêu cầu trang duy nhất.

Q: Liệu một lần đầu tiên đến trước phục vụ đơn giản hay bạn cần một cái gì đó thông minh hơn, tức là sẽ ngăn chặn nạn đói?
Trả lời: Một phân phối kiểu vòng tròn đơn giản sẽ đủ, bằng cách bỏ đói tôi giả sử bạn có nghĩa là nếu không có phiên nào có sẵn mà người gọi bị chặn chờ phát hành. Điều này không thực sự áp dụng vì các phiên có thể được chia sẻ bởi những người gọi khác nhau. Mục tiêu của tôi là phân phối việc sử dụng qua nhiều phiên thay vì 1 phiên duy nhất.

Tôi tin rằng đây có lẽ là một sự khác biệt so với cách sử dụng bình thường của nhóm đối tượng, đó là lý do ban đầu tôi bỏ phần này ra và lên kế hoạch chỉ để điều chỉnh mô hình để cho phép chia sẻ các đối tượng thay vì cho phép tình huống chết đói xảy ra.

Q: Điều gì về những thứ như ưu tiên, lười biếng so với tải háo hức, vv?
Trả lời: Không có sự ưu tiên nào liên quan, vì đơn giản chỉ cần giả định rằng tôi sẽ tạo ra nhóm đối tượng có sẵn khi tạo ra chính nhóm đó.


1
Bạn có thể cho chúng tôi biết một chút về yêu cầu của bạn? Không phải tất cả các hồ bơi được tạo ra bằng nhau. Là các đối tượng đắt tiền để tạo ra? Họ sẽ được mua / phát hành rất thường xuyên? Sẽ là một lần đầu tiên đến trước phục vụ đơn giản hoặc bạn cần một cái gì đó thông minh hơn, tức là sẽ ngăn chặn đói? Còn những thứ như ưu tiên, lười biếng so với tải háo hức, v.v.? Bất cứ điều gì bạn có thể thêm sẽ giúp chúng tôi (hoặc ít nhất là tôi) đưa ra câu trả lời kỹ lưỡng hơn.
Aaronaught

Chris - chỉ nhìn vào đoạn 2 và 3 của bạn, và tự hỏi liệu những phiên này có thực sự được giữ sống vô thời hạn không? Nghe có vẻ như đó là những gì nhà cung cấp dịch vụ của bạn không thích (các phiên chạy dài) và vì vậy bạn có thể đang tìm kiếm một triển khai nhóm để tạo ra các phiên mới khi cần thiết và tắt chúng khi không sử dụng (sau một khoảng thời gian được chỉ định) . Điều này có thể được thực hiện, nhưng phức tạp hơn một chút, vì vậy tôi muốn xác nhận.
Aaronaught

Tôi không chắc mình có cần giải pháp mạnh mẽ đó hay không vì giải pháp của tôi chỉ là giả thuyết. Có thể nhà cung cấp dịch vụ của tôi chỉ nói dối tôi và dịch vụ của họ đã bị bán hết và chỉ tìm thấy một lý do để đổ lỗi cho người dùng.
Chris Marisic

1
Tôi nghĩ rằng Bộ đệm dữ liệu TPL DataFlow thực hiện hầu hết những gì bạn cần.
tiêu

1
Nhóm trong các môi trường luồng là một vấn đề định kỳ, được giải quyết bằng các mẫu thiết kế như Resource Pool và Resource Cache. Kiểm tra Kiến trúc phần mềm hướng mẫu, Tập 3: Các mẫu để quản lý tài nguyên để biết thêm thông tin.
Fuhrmanator

Câu trả lời:


59

Đối tượng tổng hợp trong .NET Core

Các lõi DotNet có một thực hiện tổng hợp đối tượng được bổ sung vào thư viện lớp cơ sở (BCL). Bạn có thể đọc vấn đề GitHub ban đầu tại đây và xem mã cho System.Buffers . Hiện tại ArrayPoollà loại duy nhất có sẵn và được sử dụng để gộp các mảng. Có một bài viết blog tốt đẹp ở đây .

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

Một ví dụ về việc sử dụng nó có thể được nhìn thấy trong ASP.NET Core. Vì nó nằm trong lõi dotL BCL, ASP.NET Core có thể chia sẻ nhóm đối tượng của nó với các đối tượng khác như bộ tuần tự JSON của Newtonsoft.Json. Bạn có thể đọc bài đăng trên blog này để biết thêm thông tin về cách Newtonsoft.Json đang làm điều này.

Đối tượng tổng hợp trong Trình biên dịch Microsoft Roslyn C #

Trình biên dịch Microsoft Roslyn C # mới chứa loại ObjectPool , được sử dụng để gộp các đối tượng được sử dụng thường xuyên, thường sẽ được cập nhật mới và rác được thu thập rất thường xuyên. Điều này làm giảm số lượng và quy mô của các hoạt động thu gom rác phải xảy ra. Có một vài cách triển khai phụ khác nhau bằng cách sử dụng ObjectPool (Xem: Tại sao có quá nhiều triển khai Đối tượng tổng hợp trong Roslyn? ).

1 - SharedPools - Lưu trữ một nhóm gồm 20 đối tượng hoặc 100 nếu BigDefault được sử dụng.

// 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 - Không tách biệt các triển khai nhưng các trình bao bọc xung quanh triển khai SharedPools được hiển thị ở trên cụ thể cho List và StringBuilder's. Vì vậy, điều này sử dụng lại nhóm đối tượng được lưu trữ trong 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 - PooledDixiPooledHashset - Chúng sử dụng ObjectPool trực tiếp và có một nhóm đối tượng hoàn toàn riêng biệt. Lưu trữ một nhóm 128 đối tượng.

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

Thư viện này cung cấp tổng hợp cho MemoryStreamcác đối tượng. Đó là một sự thay thế thả trong cho System.IO.MemoryStream. Nó có chính xác cùng một ngữ nghĩa. Nó được thiết kế bởi các kỹ sư Bing. Đọc bài đăng trên blog ở đây hoặc xem mã trên 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); 
}

Lưu ý rằng RecyclableMemoryStreamManagernên được khai báo một lần và nó sẽ tồn tại trong toàn bộ quá trình. Đây là nhóm. Nó là hoàn toàn tốt để sử dụng nhiều hồ bơi nếu bạn muốn.


2
Đây là một câu trả lời tuyệt vời. Sau C # 6 & VS2015 là RTM, tôi có thể sẽ đưa ra câu trả lời được chấp nhận vì nó rõ ràng là câu trả lời tốt nhất nếu nó được điều chỉnh bởi chính Rosyln.
Chris Marisic

Tôi đồng ý nhưng bạn sẽ sử dụng triển khai nào? Roslyn chứa ba. Xem liên kết đến câu hỏi của tôi trong câu trả lời.
Muhammad Rehan Saeed

1
Có vẻ như mỗi mục đích đều được xác định rất rõ ràng, tốt hơn nhiều so với việc chỉ lựa chọn một kích cỡ đã mở kết thúc phù hợp với tất cả giày.
Chris Marisic

1
@MuhammadRehanSaeed bổ sung tuyệt vời với ArrayPool
Chris

1
Xem RecyclableMemoryStreamđó là một bổ sung tuyệt vời cho tối ưu hóa hiệu suất cực cao.
Chris Marisic

315

Câu hỏi này khó hơn một chút so với người ta có thể mong đợi do một số ẩn số: Hành vi của tài nguyên được gộp chung, tuổi thọ dự kiến ​​/ yêu cầu của lý do, lý do thực sự mà nhóm yêu cầu, v.v. Thông thường các nhóm là mục đích đặc biệt - luồng nhóm, nhóm kết nối, v.v. - bởi vì việc tối ưu hóa một cách dễ dàng hơn khi bạn biết chính xác tài nguyên làm gì và quan trọng hơn là có quyền kiểm soát cách thức tài nguyên đó được triển khai.

Vì nó không đơn giản, những gì tôi đã cố gắng làm là đưa ra một cách tiếp cận khá linh hoạt mà bạn có thể thử nghiệm và xem những gì hoạt động tốt nhất. Xin lỗi trước cho bài viết dài, nhưng có rất nhiều nền tảng để thực hiện khi thực hiện một nguồn tài nguyên đa năng phong nha. và tôi thực sự chỉ làm trầy xước bề mặt.

Nhóm đa năng sẽ phải có một vài "cài đặt" chính, bao gồm:

  • Chiến lược tải tài nguyên - háo hức hoặc lười biếng;
  • Cơ chế tải tài nguyên - làm thế nào để thực sự xây dựng một;
  • Chiến lược truy cập - bạn đề cập đến "vòng tròn" không đơn giản như âm thanh của nó; việc thực hiện này có thể sử dụng bộ đệm tròn tương tự , nhưng không hoàn hảo, bởi vì nhóm không có quyền kiểm soát khi tài nguyên thực sự được thu hồi. Các tùy chọn khác là FIFO và LIFO; FIFO sẽ có nhiều mẫu truy cập ngẫu nhiên hơn, nhưng LIFO giúp thực hiện chiến lược giải phóng ít sử dụng gần đây nhất (mà bạn nói là nằm ngoài phạm vi, nhưng nó vẫn đáng được đề cập).

Đối với cơ chế tải tài nguyên, .NET đã cung cấp cho chúng ta một bản tóm tắt rõ ràng - các đại biểu.

private Func<Pool<T>, T> factory;

Vượt qua điều này thông qua nhà xây dựng của hồ bơi và chúng ta sắp hoàn thành việc đó. Sử dụng một loại chung với một new()ràng buộc cũng hoạt động, nhưng điều này linh hoạt hơn.


Trong hai tham số khác, chiến lược truy cập là con thú phức tạp hơn, vì vậy cách tiếp cận của tôi là sử dụng cách tiếp cận dựa trên kế thừa (giao diện):

public class Pool<T> : IDisposable
{
    // Other code - we'll come back to this

    interface IItemStore
    {
        T Fetch();
        void Store(T item);
        int Count { get; }
    }
}

Khái niệm ở đây rất đơn giản - chúng ta sẽ để Poollớp công khai xử lý các vấn đề phổ biến như an toàn luồng, nhưng sử dụng một "kho vật phẩm" khác nhau cho mỗi mẫu truy cập. LIFO được biểu diễn dễ dàng bằng một ngăn xếp, FIFO là một hàng đợi và tôi đã sử dụng một triển khai bộ đệm vòng tròn không được tối ưu hóa nhưng có lẽ là đầy đủ bằng cách sử dụng một List<T>con trỏ và chỉ mục để xấp xỉ một mẫu truy cập vòng tròn.

Tất cả các lớp bên dưới là các lớp bên trong của Pool<T>- đây là một lựa chọn kiểu, nhưng vì chúng thực sự không được sử dụng bên ngoài Pool, nên nó có ý nghĩa nhất.

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

Đây là những cái rõ ràng - ngăn xếp và xếp hàng. Tôi không nghĩ rằng họ thực sự đảm bảo nhiều lời giải thích. Bộ đệm tròn phức tạp hơn một chút:

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

Tôi có thể đã chọn một số cách tiếp cận khác nhau, nhưng điểm mấu chốt là các tài nguyên nên được truy cập theo cùng thứ tự mà chúng được tạo, điều đó có nghĩa là chúng tôi phải duy trì các tham chiếu đến chúng nhưng đánh dấu chúng là "đang sử dụng" (hoặc không ). Trong trường hợp xấu nhất, chỉ có một vị trí có sẵn và nó sẽ lặp lại toàn bộ bộ đệm cho mỗi lần tìm nạp. Điều này thật tệ nếu bạn có hàng trăm tài nguyên được tổng hợp và đang thu thập và phát hành chúng nhiều lần trong một giây; không thực sự là một vấn đề đối với nhóm 5-10 mặt hàng và trong trường hợp điển hình , khi tài nguyên được sử dụng nhẹ, nó chỉ phải tiến một hoặc hai vị trí.

Hãy nhớ rằng, các lớp này là các lớp bên trong riêng tư - đó là lý do tại sao chúng không cần kiểm tra nhiều lỗi, chính nhóm này hạn chế quyền truy cập vào chúng.

Đưa ra một bảng liệt kê và phương pháp xuất xưởng và chúng ta đã hoàn thành phần này:

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

Vấn đề tiếp theo cần giải quyết là tải chiến lược. Tôi đã xác định ba loại:

public enum LoadingMode { Eager, Lazy, LazyExpanding };

Hai cái đầu tiên nên tự giải thích; thứ ba là một loại hỗn hợp, nó lười tải tài nguyên nhưng thực tế không bắt đầu sử dụng lại bất kỳ tài nguyên nào cho đến khi nhóm đầy. Đây sẽ là một sự đánh đổi tốt nếu bạn muốn hồ bơi đầy (có vẻ như bạn làm) nhưng muốn trì hoãn chi phí thực sự tạo ra chúng cho đến khi truy cập lần đầu (tức là để cải thiện thời gian khởi động).

Các phương thức tải thực sự không quá phức tạp, bây giờ chúng ta có sự trừu tượng hóa cửa hàng vật phẩm:

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

Các trường sizecountở trên đề cập đến kích thước tối đa của nhóm và tổng số tài nguyên thuộc sở hữu của nhóm (nhưng không nhất thiết có sẵn ), tương ứng. AcquireEagerlà đơn giản nhất, nó giả định rằng một mặt hàng đã có trong cửa hàng - những mặt hàng này sẽ được tải sẵn khi xây dựng, tức là trong PreloadItemsphương thức hiển thị cuối cùng.

AcquireLazykiểm tra xem có các mục miễn phí trong nhóm không, và nếu không, nó sẽ tạo một mục mới. AcquireLazyExpandingsẽ tạo một tài nguyên mới miễn là nhóm chưa đạt được kích thước mục tiêu. Tôi đã cố gắng tối ưu hóa điều này để giảm thiểu việc khóa và tôi hy vọng tôi đã không mắc phải bất kỳ lỗi nào (tôi đã thử nghiệm điều này trong các điều kiện đa luồng, nhưng rõ ràng là không triệt để).

Bạn có thể tự hỏi tại sao không có phương pháp nào trong số các phương pháp này bận tâm kiểm tra xem liệu cửa hàng có đạt kích thước tối đa hay không. Tôi sẽ đến đó trong giây lát.


Bây giờ cho hồ bơi chính nó. Dưới đây là bộ dữ liệu cá nhân đầy đủ, một số dữ liệu đã được hiển thị:

    private bool isDisposed;
    private Func<Pool<T>, T> factory;
    private LoadingMode loadingMode;
    private IItemStore itemStore;
    private int size;
    private int count;
    private Semaphore sync;

Trả lời câu hỏi tôi đã trình bày trong đoạn cuối - làm thế nào để đảm bảo chúng tôi giới hạn tổng số tài nguyên được tạo - hóa ra .NET đã có một công cụ hoàn toàn tốt cho điều đó, nó được gọi là Semaphore và nó được thiết kế đặc biệt để cho phép cố định số lượng luồng truy cập vào một tài nguyên (trong trường hợp này là "tài nguyên" là kho lưu trữ vật phẩm bên trong). Vì chúng tôi không triển khai hàng đợi nhà sản xuất / người tiêu dùng đầy đủ, điều này hoàn toàn phù hợp với nhu cầu của chúng tôi.

Hàm tạo trông như thế này:

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

Không có gì đáng ngạc nhiên ở đây. Điều duy nhất cần lưu ý là vỏ đặc biệt để tải háo hức, sử dụng PreloadItemsphương pháp đã được hiển thị trước đó.

Vì hầu hết mọi thứ đã được trừu tượng hóa ngay bây giờ, nên thực tế AcquireReleasephương pháp thực sự rất đơn giản:

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

Như đã giải thích trước đó, chúng tôi đang sử dụng Semaphoređể kiểm soát đồng thời thay vì kiểm tra một cách tôn giáo trạng thái của cửa hàng vật phẩm. Miễn là các mặt hàng thu được được phát hành chính xác, không có gì phải lo lắng.

Cuối cùng nhưng không kém phần quan trọng, có dọn dẹp:

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

Mục đích của IsDisposedtài sản đó sẽ trở nên rõ ràng trong giây lát. Tất cả các Disposephương pháp chính thực sự là xử lý các mục tổng hợp thực tế nếu chúng thực hiện IDisposable.


Bây giờ về cơ bản bạn có thể sử dụng nguyên trạng này với một try-finallykhối, nhưng tôi không thích cú pháp đó, bởi vì nếu bạn bắt đầu chuyển xung quanh các tài nguyên được gộp giữa các lớp và các phương thức thì nó sẽ trở nên rất khó hiểu. Có thể lớp chính sử dụng tài nguyên thậm chí không tham chiếu đến nhóm. Nó thực sự trở nên khá lộn xộn, vì vậy một cách tiếp cận tốt hơn là tạo ra một đối tượng gộp "thông minh".

Giả sử chúng ta bắt đầu với giao diện / lớp đơn giản sau:

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

Đây là Footài nguyên dùng một lần của chúng tôi thực hiện IFoovà có một số mã soạn sẵn để tạo danh tính duy nhất. Những gì chúng ta làm là tạo ra một đối tượng đặc biệt, gộp lại:

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

Điều này chỉ ủy thác tất cả các phương thức "thực" vào bên trong của nó IFoo(chúng ta có thể thực hiện điều này với thư viện Proxy động như Castle, nhưng tôi sẽ không tham gia vào đó). Nó cũng duy trì một tham chiếu đến Poolcái tạo ra nó, để khi chúng ta Disposeđối tượng này, nó sẽ tự động giải phóng trở lại nhóm. Ngoại trừ khi hồ bơi đã được xử lý - điều này có nghĩa là chúng tôi đang ở chế độ "dọn dẹp" và trong trường hợp này, nó thực sự làm sạch tài nguyên nội bộ thay thế.


Sử dụng cách tiếp cận ở trên, chúng ta có thể viết mã như thế này:

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

Đây là một điều rất tốt để có thể làm. Nó có nghĩa là mã mà sử dụng các IFoo(như trái ngược với các mã mà tạo ra nó) không thực sự cần phải nhận thức của hồ bơi. Bạn thậm chí có thể tiêm IFoo các đối tượng bằng thư viện DI yêu thích của mình và Pool<T>là nhà cung cấp / nhà máy.


Tôi đã đặt mã hoàn chỉnh trên PasteBin để thưởng thức sao chép và dán của bạn. Ngoài ra còn có một chương trình thử nghiệm ngắn mà bạn có thể sử dụng để chơi xung quanh với các chế độ tải / truy cập khác nhau và các điều kiện đa luồng, để thỏa mãn bản thân rằng nó an toàn cho chủ đề và không có lỗi.

Hãy cho tôi biết nếu bạn có bất kỳ câu hỏi hoặc quan tâm về bất kỳ điều này.


62
Một trong những câu trả lời đầy đủ, hữu ích và thú vị nhất mà tôi đã đọc trên SO.
Josh Smeaton

Tôi không thể đồng ý nhiều hơn với @Josh về phản hồi này, đặc biệt là đối với phần PooledFoo vì việc phát hành các đối tượng dường như luôn được xử lý theo cách rất rò rỉ và tôi đã tưởng tượng rằng sẽ có ý nghĩa nhất khi sử dụng nó xây dựng như bạn cho thấy tôi chỉ không ngồi xuống và cố gắng xây dựng mà khi câu trả lời của bạn cung cấp cho tôi tất cả thông tin tôi có thể cần để giải quyết vấn đề của mình. Tôi nghĩ đối với tình huống cụ thể của mình, tôi sẽ có thể đơn giản hóa điều này khá nhiều vì tôi có thể chia sẻ các thể hiện giữa các luồng và không cần phải giải phóng chúng trở lại nhóm.
Chris Marisic

Tuy nhiên, nếu cách tiếp cận đơn giản không hoạt động trước tiên, tôi có một vài ý tưởng trong đầu về cách tôi có thể xử lý việc phát hành một cách thông minh cho trường hợp của mình. Tôi nghĩ cụ thể nhất là tôi sẽ thiết lập bản phát hành để có thể xác định rằng phiên đó đã bị lỗi và loại bỏ nó và thay thế một phiên bản mới vào nhóm. Bất kể bài đăng này vào thời điểm này có khá nhiều hướng dẫn dứt khoát về việc tập hợp đối tượng trong C # 3.0, tôi rất mong được xem có ai có nhiều bình luận hơn về điều này không.
Chris Marisic

@Chris: Nếu bạn đang nói về proxy khách hàng WCF thì tôi cũng có một mô hình cho điều đó, mặc dù bạn cần một trình tiêm phụ thuộc hoặc trình chặn phương thức để sử dụng nó một cách hiệu quả. Phiên bản DI sử dụng kernel với nhà cung cấp tùy chỉnh để có được phiên bản mới nếu bị lỗi, phiên bản chặn phương thức (tùy chọn của tôi) chỉ bọc một proxy hiện có và chèn kiểm tra lỗi trước mỗi proxy. Tôi không chắc việc tích hợp nó vào một bể như thế này sẽ dễ dàng như thế nào (chưa thực sự cố gắng, vì tôi chỉ viết cái này!) Nhưng chắc chắn là có thể.
Aaronaught

5
Rất ấn tượng, mặc dù hơi quá kỹ thuật cho hầu hết các tình huống. Tôi hy vọng một cái gì đó như thế này sẽ là một phần của khung.
ChaosPandion

7

Một cái gì đó như thế này có thể phù hợp với nhu cầu của bạn.

/// <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)
                {

                }
            }
        }
    }
}

Cách sử dụng ví dụ

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
Cào mà bình luận trước đó. Tôi nghĩ rằng tôi chỉ thấy lạ vì hồ bơi này dường như không có bất kỳ ngưỡng nào và có lẽ nó không cần thiết, nó sẽ phụ thuộc vào các yêu cầu.
Aaronaught

1
@Aaronaught - Có thật là kỳ quặc không? Tôi muốn tạo ra một nhóm nhẹ chỉ cung cấp các chức năng cần thiết. Tùy thuộc vào khách hàng để sử dụng lớp đúng cách.
ChaosPandion

1
+1 cho một giải pháp rất đơn giản có thể phù hợp với mục đích của tôi bằng cách thay đổi loại sao lưu thành Danh sách / HashTable, v.v. và thay đổi bộ đếm để cuộn lại. Câu hỏi ngẫu nhiên làm thế nào để bạn xử lý quản lý của chính đối tượng pool? Bạn chỉ cần dán nó vào một thùng chứa IOC xác định nó là singleton ở đó?
Chris Marisic

1
Có nên đọc tĩnh? Nhưng tôi thấy thật kỳ lạ khi bạn sẽ đưa vào một tuyên bố cuối cùng, nếu có một ngoại lệ thì có khả năng chính đối tượng mà nó tự bị lỗi không? Bạn có thể xử lý nó bên trong Putphương thức và để nó đơn giản một số loại kiểm tra xem đối tượng có bị lỗi hay không và để tạo một thể hiện mới được thêm vào nhóm thay vì chèn trước đó?
Chris Marisic

1
@Chris - Tôi chỉ đơn giản là cung cấp một công cụ đơn giản mà tôi thấy hữu ích trong quá khứ. Phần còn lại là tùy thuộc vào bạn. Sửa đổi và sử dụng mã khi bạn thấy phù hợp.
ChaosPandion

6

Cảm ơn liên kết đó. Không có giới hạn kích thước cho việc triển khai này, vì vậy nếu bạn có sự tăng đột biến trong việc tạo đối tượng, những trường hợp đó sẽ không bao giờ được thu thập và có thể không bao giờ được sử dụng cho đến khi có một đột biến khác. Mặc dù đây là một điều rất đơn giản và dễ hiểu và sẽ không khó để thêm giới hạn kích thước tối đa.
Muhammad Rehan Saeed

Đẹp và đơn giản
Daniel de Zwaan

4

Trước đây, Microsoft đã cung cấp một khung thông qua Microsoft Transaction Server (MTS) và sau đó là COM + để thực hiện gộp đối tượng cho các đối tượng COM. Chức năng đó đã được chuyển tiếp tới System. EntrypriseService trong .NET Framework và giờ là trong Windows Communication Foundation.

Đối tượng Pooling trong WCF

Bài viết này từ .NET 1.1 nhưng vẫn nên áp dụng trong các phiên bản hiện tại của Framework (mặc dù WCF là phương pháp ưa thích).

Đối tượng tổng hợp .NET


+1 để cho tôi thấy rằng IInstanceProvidergiao diện tồn tại vì tôi sẽ thực hiện điều này cho giải pháp của mình. Tôi luôn là người hâm mộ sắp xếp mã của mình đằng sau giao diện do Microsoft cung cấp khi họ cung cấp định nghĩa phù hợp.
Chris Marisic

4

Tôi thực sự thích việc triển khai của Aronaught - đặc biệt là khi anh ta xử lý việc chờ đợi tài nguyên để sẵn sàng thông qua việc sử dụng một semaphore. Có một số bổ sung tôi muốn thực hiện:

  1. Thay đổi sync.WaitOne()để sync.WaitOne(timeout)và phơi bày thời gian chờ như một tham số vào Acquire(int timeout)phương pháp. Điều này cũng đòi hỏi phải xử lý điều kiện khi luồng hết thời gian chờ đợi trên một đối tượng để sẵn sàng.
  2. Thêm Recycle(T item)phương thức để xử lý các tình huống khi một đối tượng cần được tái chế khi xảy ra lỗi.

3

Đây là một triển khai khác, với số lượng đối tượng hạn chế trong nhóm.

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



Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.