Implementasi Pola Pooling Objek C #

165

Adakah yang punya sumber daya yang bagus untuk menerapkan strategi kumpulan objek bersama untuk sumber daya terbatas sesuai dengan kumpulan koneksi Sql? (Yaitu akan dilaksanakan sepenuhnya bahwa itu aman thread).

Untuk menindaklanjuti berkenaan dengan permintaan @Aaronaught untuk klarifikasi, penggunaan kumpulan adalah untuk permintaan penyeimbangan beban ke layanan eksternal. Untuk memasukkannya ke dalam skenario yang mungkin akan lebih mudah untuk segera dipahami sebagai lawan dari situasi langsung saya. Saya memiliki objek sesi yang fungsinya mirip dengan ISessionobjek dari NHibernate. Bahwa setiap sesi unik mengelola koneksi ke database. Saat ini saya memiliki 1 objek sesi berjalan lama dan saya menghadapi masalah di mana penyedia layanan saya membatasi penggunaan saya pada sesi individual ini.

Karena kurangnya harapan mereka bahwa satu sesi akan diperlakukan sebagai akun layanan yang berjalan lama, mereka tampaknya memperlakukannya sebagai klien yang memalu layanan mereka. Yang membawa saya ke pertanyaan saya di sini, alih-alih memiliki 1 sesi individu saya akan membuat kumpulan sesi yang berbeda dan membagi permintaan ke layanan di beberapa sesi daripada bukannya membuat satu titik fokus seperti yang saya lakukan sebelumnya.

Semoga latar belakang itu menawarkan nilai tetapi untuk langsung menjawab beberapa pertanyaan Anda:

T: Apakah benda-benda mahal untuk dibuat?
A: Tidak ada objek yang merupakan kumpulan sumber daya terbatas

T: Apakah mereka akan diperoleh / dirilis sangat sering?
A: Ya, sekali lagi mereka dapat dianggap sebagai NHibernate ISessions di mana 1 biasanya diperoleh dan dirilis selama setiap permintaan satu halaman.

T: Apakah layanan first-first-first-serve yang sederhana atau apakah Anda memerlukan sesuatu yang lebih cerdas, yaitu yang akan mencegah kelaparan?
A: Distribusi tipe robin bulat sederhana sudah cukup, dengan kelaparan saya anggap Anda maksud jika tidak ada sesi yang tersedia sehingga penelepon diblokir menunggu rilis. Ini tidak benar-benar berlaku karena sesi dapat dibagikan oleh penelepon yang berbeda. Tujuan saya adalah mendistribusikan penggunaannya di beberapa sesi yang bertentangan dengan 1 sesi tunggal.

Saya percaya ini mungkin merupakan penyimpangan dari penggunaan normal dari kumpulan objek yang mengapa saya awalnya meninggalkan bagian ini dan merencanakan hanya untuk mengadaptasi pola untuk memungkinkan berbagi objek sebagai lawan memungkinkan situasi kelaparan yang pernah terjadi.

T: Bagaimana dengan hal-hal seperti prioritas, pemuatan malas vs bersemangat, dll.?
A: Tidak ada prioritisasi yang terlibat, demi kesederhanaan hanya berasumsi bahwa saya akan membuat kumpulan objek yang tersedia pada pembuatan pool itu sendiri.

Chris Marisic
sumber
1
Bisakah Anda memberi tahu kami sedikit tentang kebutuhan Anda? Tidak semua kolam dibuat sama. Apakah benda mahal untuk dibuat? Apakah mereka akan diperoleh / dirilis sangat sering? Akankah layanan first-first-first-serve yang sederhana atau apakah Anda memerlukan sesuatu yang lebih cerdas, yaitu yang akan mencegah kelaparan? Bagaimana dengan hal-hal seperti prioritas, pemuatan malas vs bersemangat, dll.? Apa pun yang dapat Anda tambahkan akan membantu kami (atau setidaknya saya) untuk memberikan jawaban yang lebih menyeluruh.
Aaronaught
Chris - hanya melihat paragraf 2 dan 3 Anda, dan bertanya-tanya apakah sesi ini benar-benar harus tetap hidup tanpa batas waktu? Kedengarannya seperti itulah yang tidak disukai penyedia layanan Anda (sesi berjalan lama), jadi Anda mungkin mencari implementasi kumpulan yang memutar sesi baru seperlunya dan mematikannya saat tidak digunakan (setelah beberapa periode tertentu) . Ini bisa dilakukan, tetapi sedikit lebih rumit, jadi saya ingin mengkonfirmasi.
Aaronaught
Saya tidak yakin apakah saya membutuhkan solusi yang kuat atau belum karena solusi saya hanyalah hipotesis. Mungkin saja penyedia layanan saya hanya berbohong kepada saya dan bahwa layanan mereka sudah terjual habis dan hanya menemukan alasan untuk menyalahkan pengguna.
Chris Marisic
1
Saya pikir TPL DataFlow BufferBlock melakukan sebagian besar dari apa yang Anda butuhkan.
pemboros
1
Penggabungan dalam lingkungan berulir adalah masalah berulang, diselesaikan dengan pola desain seperti Resource Pool dan Resource Cache. Lihat Arsitektur Perangkat Lunak Berorientasi Pola, Volume 3: Pola untuk Manajemen Sumber Daya untuk info lebih lanjut.
Fuhrmanator

Jawaban:

59

Penyatuan Objek dalam .NET Core

The dotnet inti memiliki implementasi objek pooling ditambahkan ke perpustakaan kelas dasar (BCL). Anda dapat membaca masalah GitHub asli di sini dan melihat kode untuk System.Buffers . Saat ini ArrayPooladalah satu-satunya jenis yang tersedia dan digunakan untuk mengumpulkan array. Ada posting blog yang bagus di sini .

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

Contoh penggunaannya dapat dilihat di ASP.NET Core. Karena berada dalam BCL dotnet core, ASP.NET Core dapat berbagi kumpulan objek itu dengan objek lain seperti JSON serializer Newtonsoft.Json. Anda dapat membaca posting blog ini untuk informasi lebih lanjut tentang bagaimana Newtonsoft.Json melakukan ini.

Pooling Objek di Microsoft Roslyn C # Compiler

Kompiler Microsoft Roslyn C # yang baru berisi tipe ObjectPool , yang digunakan untuk menyatukan objek yang sering digunakan yang biasanya mendapatkan yang baru dan sampah dikumpulkan sangat sering. Ini mengurangi jumlah dan ukuran operasi pengumpulan sampah yang harus terjadi. Ada beberapa sub-implementasi yang berbeda menggunakan ObjectPool (Lihat: Mengapa ada begitu banyak implementasi Object Pooling di Roslyn? ).

1 - SharedPools - Menyimpan kumpulan 20 objek atau 100 jika BigDefault digunakan.

// 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 - ListPool dan StringBuilderPool - Tidak sepenuhnya memisahkan implementasi tetapi pembungkus di sekitar implementasi SharedPools yang ditunjukkan di atas khusus untuk List dan StringBuilder. Jadi ini menggunakan kembali kumpulan objek yang disimpan dalam 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 - PooledDictionary dan PooledHashSet - Ini menggunakan ObjectPool secara langsung dan memiliki kumpulan objek yang benar-benar terpisah. Menyimpan kumpulan 128 objek.

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

Perpustakaan ini menyediakan kumpulan untuk MemoryStreamobjek. Ini pengganti pengganti untuk System.IO.MemoryStream. Memiliki semantik yang persis sama. Ini dirancang oleh insinyur Bing. Baca posting blog di sini atau lihat kode di 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); 
}

Catatan yang RecyclableMemoryStreamManagerharus dideklarasikan sekali dan itu akan hidup untuk seluruh proses - ini adalah kolam. Sangatlah baik untuk menggunakan beberapa kolam jika Anda inginkan.

Muhammad Rehan Saeed
sumber
2
Ini jawaban yang bagus. Setelah C # 6 & VS2015 adalah RTM saya mungkin akan membuat ini jawaban yang diterima karena jelas yang terbaik dari semua jika begitu disetel digunakan oleh Rosyln sendiri.
Chris Marisic
Saya setuju tetapi implementasi mana yang akan Anda gunakan? Roslyn mengandung tiga. Lihat tautan ke pertanyaan saya di jawabannya.
Muhammad Rehan Saeed
1
Sepertinya masing-masing memiliki tujuan yang sangat jelas, jauh lebih baik daripada hanya pilihan satu ukuran terbuka untuk semua sepatu.
Chris Marisic
1
@MuhammadRehanSaeed tambahan yang bagus dengan ArrayPool
Chris Marisic
1
Melihat RecyclableMemoryStreamitu adalah tambahan yang luar biasa untuk optimisasi kinerja sangat tinggi.
Chris Marisic
315

Pertanyaan ini sedikit lebih rumit daripada yang diperkirakan karena beberapa hal yang tidak diketahui: Perilaku sumber daya yang dikumpulkan, umur objek yang diharapkan / diperlukan, alasan sebenarnya bahwa kolam diperlukan, dll. Biasanya kolam adalah tujuan khusus - utas pool, pool koneksi, dll. - karena lebih mudah untuk mengoptimalkannya ketika Anda tahu persis apa yang sumber daya lakukan dan yang lebih penting memiliki kontrol atas bagaimana sumber daya itu diimplementasikan.

Karena itu tidak sesederhana itu, apa yang saya coba lakukan adalah menawarkan pendekatan yang cukup fleksibel yang dapat Anda coba dan lihat apa yang paling berhasil. Permintaan maaf di muka untuk posting lama, tetapi ada banyak alasan untuk membahas penerapan sumber daya tujuan umum yang layak. dan aku benar-benar hanya menggaruk permukaan.

Kumpulan tujuan umum harus memiliki beberapa "pengaturan" utama, termasuk:

  • Strategi pemuatan sumber daya - bersemangat atau malas;
  • Mekanisme pemuatan sumber daya - bagaimana cara membangunnya;
  • Strategi akses - Anda menyebutkan "round robin" yang tidak semudah kedengarannya; implementasi ini dapat menggunakan buffer lingkaran yang serupa , tetapi tidak sempurna, karena kumpulan tidak memiliki kendali atas kapan sumber daya benar-benar direklamasi. Opsi lainnya adalah FIFO dan LIFO; FIFO akan memiliki lebih banyak pola akses acak, tetapi LIFO membuatnya secara signifikan lebih mudah untuk menerapkan strategi pembebasan yang paling jarang digunakan (yang Anda katakan berada di luar jangkauan, tetapi masih layak disebutkan).

Untuk mekanisme pemuatan sumber daya, .NET sudah memberi kami abstraksi bersih - delegasi.

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

Lewati ini melalui konstruktor kolam dan kita hampir selesai dengan itu. Menggunakan tipe generik dengan anew() kendala juga berfungsi, tetapi ini lebih fleksibel.


Dari dua parameter lainnya, strategi akses adalah binatang yang lebih rumit, jadi pendekatan saya adalah menggunakan pendekatan berbasis warisan (antarmuka):

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

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

Konsep di sini sederhana - kami akan membiarkan publik Pool kelas menangani masalah umum seperti keamanan utas, tetapi menggunakan "toko barang" yang berbeda untuk setiap pola akses. LIFO mudah diwakili oleh stack, FIFO adalah antrian, dan saya telah menggunakan implementasi buffer bundar yang tidak terlalu dioptimalkan tetapi menggunakan List<T>pointer dan indeks pointer untuk memperkirakan pola akses round-robin.

Semua kelas di bawah ini adalah kelas dalam dari Pool<T>- ini adalah pilihan gaya, tetapi karena ini benar-benar tidak dimaksudkan untuk digunakan di luar Pool, itu paling masuk akal.

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

Ini adalah yang jelas - tumpukan dan antrian. Saya tidak berpikir mereka benar-benar membutuhkan banyak penjelasan. Buffer bundar sedikit lebih rumit:

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

Saya bisa saja memilih sejumlah pendekatan yang berbeda, tetapi intinya adalah bahwa sumber daya harus diakses dalam urutan yang sama dengan yang dibuat, yang berarti bahwa kita harus mempertahankan referensi kepada mereka tetapi menandainya sebagai "sedang digunakan" (atau tidak ). Dalam skenario terburuk, hanya satu slot yang pernah tersedia, dan dibutuhkan pengulangan buffer penuh untuk setiap pengambilan. Ini buruk jika Anda memiliki ratusan sumber daya yang dikumpulkan dan memperoleh dan melepaskannya beberapa kali per detik; tidak benar-benar masalah untuk kumpulan 5-10 item, dan di kasus khas , di mana sumber daya digunakan dengan ringan, hanya perlu memajukan satu atau dua slot.

Ingat, kelas-kelas ini adalah kelas batin pribadi - itulah sebabnya mereka tidak perlu banyak memeriksa kesalahan, kumpulan itu sendiri membatasi akses ke mereka.

Lemparkan ke dalam enumerasi dan metode pabrik dan kita selesai dengan bagian ini:

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

Masalah selanjutnya yang harus dipecahkan adalah strategi pemuatan. Saya telah mendefinisikan tiga jenis:

public enum LoadingMode { Eager, Lazy, LazyExpanding };

Dua yang pertama harus jelas; yang ketiga adalah semacam hibrida, itu memuat sumber daya tetapi tidak benar-benar mulai menggunakan kembali sumber daya apa pun sampai kolam penuh. Ini akan menjadi trade-off yang baik jika Anda ingin kumpulan menjadi penuh (yang sepertinya Anda lakukan) tetapi ingin menunda biaya untuk benar-benar membuatnya sampai akses pertama (yaitu untuk meningkatkan waktu startup).

Metode pemuatan benar-benar tidak terlalu rumit, karena sekarang kami memiliki abstraksi 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;
    }

Bidang sizedan countdi atas merujuk pada ukuran maksimum kumpulan dan jumlah total sumber daya yang dimiliki oleh kumpulan ( masing-masing tidak harus tersedia ). AcquireEageradalah yang paling sederhana, diasumsikan bahwa suatu barang sudah ada di toko - barang-barang ini akan dimuat pada saat konstruksi, yaitu diPreloadItems metode yang ditunjukkan terakhir.

AcquireLazymemeriksa untuk melihat apakah ada item gratis di kolam renang, dan jika tidak, itu membuat yang baru. AcquireLazyExpandingakan membuat sumber daya baru selama pool belum mencapai ukuran targetnya. Saya sudah mencoba mengoptimalkan ini untuk meminimalkan penguncian, dan saya harap saya tidak membuat kesalahan (saya punya menguji ini di bawah kondisi multi-threaded, tapi jelas tidak lengkap).

Anda mungkin bertanya-tanya mengapa tidak ada metode ini yang repot-repot memeriksa untuk melihat apakah toko telah mencapai ukuran maksimum atau tidak. Saya akan membahasnya sebentar lagi.


Sekarang untuk kolam itu sendiri. Berikut ini adalah set lengkap data pribadi, beberapa di antaranya telah ditunjukkan:

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

Menjawab pertanyaan yang saya sampaikan pada paragraf terakhir - bagaimana memastikan kita membatasi jumlah total sumber daya yang dibuat - ternyata .NET sudah memiliki alat yang sangat bagus untuk itu, itu disebut Semaphore dan dirancang khusus untuk memungkinkan perbaikan jumlah utas akses ke sumber daya (dalam hal ini "sumber daya" adalah toko item dalam). Karena kita tidak menerapkan antrian produsen / konsumen sepenuhnya, ini sangat memadai untuk kebutuhan kita.

Konstruktornya terlihat seperti ini:

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

Seharusnya tidak ada kejutan di sini. Satu-satunya hal yang perlu diperhatikan adalah casing khusus untuk eager loading, menggunakanPreloadItems metode yang sudah ditunjukkan sebelumnya.

Karena hampir semuanya telah diabstraksi secara bersih sekarang, metode Acquiredan aktualnya Releasesangat mudah:

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

Seperti yang dijelaskan sebelumnya, kami menggunakan Semaphoreuntuk mengontrol konkurensi alih-alih memeriksa status toko item secara agama. Selama item yang diperoleh dirilis dengan benar, tidak ada yang perlu dikhawatirkan.

Terakhir, ada pembersihan:

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

Tujuan dari IsDisposedproperti itu akan menjadi jelas dalam sekejap. Semua Disposemetode utama yang benar-benar dilakukan adalah membuang item yang dikumpulkan jika mereka menerapkan IDisposable.


Sekarang Anda pada dasarnya dapat menggunakan ini apa adanya, dengan sebuah try-finallyblok, tapi saya tidak menyukai sintaks itu, karena jika Anda mulai membagikan sumber daya yang dikumpulkan antara kelas dan metode maka itu akan menjadi sangat membingungkan. Mungkin saja kelas utama yang menggunakan sumber daya tidak memilikinya referensi ke kumpulan. Ini benar-benar menjadi sangat berantakan, jadi pendekatan yang lebih baik adalah membuat objek yang dikumpulkan "pintar".

Katakanlah kita mulai dengan antarmuka / kelas sederhana berikut:

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

Inilah Foosumber daya pura-pura sekali pakai kami yang mengimplementasikan IFoodan memiliki beberapa kode boilerplate untuk menghasilkan identitas unik. Apa yang kami lakukan adalah membuat objek khusus yang dikumpulkan:

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

Ini hanya proksi semua metode "nyata" ke dalamnya IFoo(kita bisa melakukan ini dengan perpustakaan Proxy Dinamis seperti Castle, tapi saya tidak akan membahasnya). Itu juga memelihara referensi ke Poolyang membuatnya, sehingga ketika kita Disposeobjek ini, secara otomatis melepaskan dirinya kembali ke kolam. Kecuali ketika pool telah dibuang - ini berarti kita berada dalam mode "pembersihan" dan dalam hal ini sebenarnya membersihkan sumber daya internal sebagai gantinya.


Dengan menggunakan pendekatan di atas, kita dapat menulis kode seperti ini:

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

Ini adalah hal yang sangat baik untuk dapat dilakukan. Ini berarti bahwa kode yang menggunakan satu IFoo(yang bertentangan dengan kode yang menciptakan itu) tidak benar-benar perlu menyadari dari kolam renang. Anda bahkan dapat menyuntikkan IFoo objek menggunakan perpustakaan DI favorit Anda dan Pool<T>sebagai penyedia / pabrik.


Saya telah memasukkan kode lengkap pada PasteBin untuk kesenangan copy-and-paste Anda. Ada juga program pengujian singkat yang dapat Anda gunakan untuk bermain-main dengan berbagai mode pemuatan / akses dan kondisi multithreaded, untuk meyakinkan diri sendiri bahwa ini aman untuk benang dan tidak bermasalah.

Beri tahu saya jika Anda memiliki pertanyaan atau masalah tentang semua ini.

Aaronaught
sumber
62
Salah satu jawaban paling lengkap, bermanfaat, dan menarik yang pernah saya baca di SO.
Josh Smeaton
Saya sangat setuju dengan @Josh tentang tanggapan ini, terutama untuk bagian PooledFoo karena melepaskan benda-benda sepertinya selalu ditangani dengan cara yang sangat bocor dan saya membayangkan bahwa akan lebih masuk akal untuk memungkinkannya menggunakan menggunakan membangun seperti yang Anda tunjukkan saya baru saja tidak duduk dan mencoba membangun bahwa ketika jawaban Anda memberi saya semua informasi yang saya butuhkan untuk menyelesaikan masalah saya. Saya pikir untuk situasi spesifik saya, saya akan dapat menyederhanakan ini sedikit karena saya dapat membagikan contoh di antara utas dan tidak perlu melepaskannya kembali ke kolam.
Chris Marisic
Namun jika pendekatan sederhana tidak berhasil terlebih dahulu, saya punya beberapa ide di kepala saya tentang bagaimana saya bisa dengan cerdas menangani pembebasan untuk kasus saya. Saya pikir yang paling khusus saya akan membuat rilis untuk dapat menentukan bahwa sesi itu sendiri salah dan membuangnya dan mengganti yang baru ke dalam kumpulan. Terlepas dari posting ini pada titik ini cukup banyak panduan definitif tentang penyatuan objek di C # 3.0, saya berharap untuk melihat apakah ada orang lain yang memiliki lebih banyak komentar tentang ini.
Chris Marisic
@ Chris: Jika Anda berbicara tentang proksi klien WCF maka saya memiliki pola untuk itu juga, meskipun Anda memerlukan injektor ketergantungan atau metode pencegat untuk menggunakannya secara efektif. Versi DI menggunakan kernel dengan penyedia kustom untuk mendapatkan versi baru jika salah, versi metode intersepsi (preferensi saya) hanya membungkus proxy yang ada dan memasukkan pemeriksaan kesalahan sebelum masing-masing. Saya tidak yakin betapa mudahnya untuk mengintegrasikannya ke dalam kumpulan seperti ini (belum benar-benar mencoba, karena saya baru saja menulis ini!) Tetapi pasti akan mungkin.
Aaronaught
5
Sangat mengesankan, meskipun sedikit over-engineered untuk sebagian besar situasi. Saya berharap sesuatu seperti ini menjadi bagian dari kerangka kerja.
ChaosPandion
7

Sesuatu seperti ini mungkin sesuai dengan kebutuhan Anda.

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

                }
            }
        }
    }
}

Contoh Penggunaan

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);
        }
    }
}
Kekacauan Kekacauan
sumber
1
Gores komentar sebelumnya. Saya pikir saya merasa aneh karena kumpulan ini tampaknya tidak memiliki ambang batas, dan mungkin tidak perlu, itu akan tergantung pada persyaratan.
Aaronaught
1
@Aaronaught - Apakah ini benar-benar aneh? Saya ingin membuat kolam ringan yang hanya menawarkan fungsionalitas yang diperlukan. Terserah klien untuk menggunakan kelas dengan benar.
ChaosPandion
1
+1 untuk solusi yang sangat sederhana yang dapat disesuaikan dengan tujuan saya hanya dengan mengubah jenis dukungan menjadi Daftar / HashTable dll dan mengubah penghitung untuk bergulir. Pertanyaan acak bagaimana Anda menangani manajemen objek kolam itu sendiri? Apakah Anda hanya memasukkannya ke dalam wadah IOC yang mendefinisikannya sebagai singleton di sana?
Chris Marisic
1
Haruskah itu hanya baca statis? Tapi saya merasa aneh bahwa Anda akan memasukkan put dalam pernyataan akhirnya, jika ada pengecualian bukankah itu mungkin bahwa objek itu sendiri salah? Apakah Anda menangani itu di dalam Putmetode dan meninggalkannya untuk kesederhanaan beberapa jenis pemeriksaan apakah objek tersebut rusak dan untuk membuat contoh baru yang akan ditambahkan ke kumpulan bukan memasukkan sebelumnya?
Chris Marisic
1
@ Chris - Saya hanya menawarkan alat sederhana yang menurut saya berguna di masa lalu. Sisanya terserah padamu. Ubah dan gunakan kode sesuai keinginan Anda.
ChaosPandion
6

Contoh dari MSDN: Cara: Membuat Pool Objek dengan Menggunakan ConcurrentBag

Thomas Mutzl
sumber
Terima kasih atas tautannya. Tidak ada batasan ukuran untuk implementasi ini, jadi jika Anda memiliki lonjakan dalam pembuatan objek, contoh-contoh itu tidak akan pernah dikumpulkan dan mungkin tidak pernah digunakan sampai ada lonjakan lain. Ini adalah sangat sederhana dan mudah dimengerti dan tidak akan sulit untuk menambahkan batas ukuran maksimum.
Muhammad Rehan Saeed
Bagus dan sederhana
Daniel de Zwaan
4

Kembali pada hari Microsoft menyediakan kerangka kerja melalui Microsoft Transaction Server (MTS) dan kemudian COM + untuk melakukan pengumpulan objek untuk objek COM. Fungsionalitas itu dibawa ke System.EnterpriseServices di .NET Framework dan sekarang di Windows Communication Foundation.

Pool Objek di WCF

Artikel ini dari. NET 1.1 tetapi harus tetap berlaku dalam versi Kerangka saat ini (meskipun WCF adalah metode yang disukai).

Objek Pooling .NET

Thomas
sumber
+1 untuk menunjukkan kepada saya bahwa IInstanceProviderantarmuka ada karena saya akan menerapkan ini untuk solusi saya. Saya selalu penggemar menumpuk kode saya di belakang antarmuka yang disediakan Microsoft ketika mereka memberikan definisi yang pas.
Chris Marisic
4

Saya sangat suka implementasi Aronaught - terutama karena ia menangani menunggu sumber daya menjadi tersedia melalui penggunaan semafor. Ada beberapa tambahan yang ingin saya buat:

  1. Ubah sync.WaitOne()ke sync.WaitOne(timeout)dan tampilkan batas waktu sebagai parameter pada Acquire(int timeout)metode. Ini juga akan memerlukan penanganan kondisi ketika utas habis menunggu suatu objek tersedia.
  2. Tambahkan Recycle(T item)metode untuk menangani situasi ketika suatu objek perlu didaur ulang ketika kegagalan terjadi, misalnya.
Igor Pashchuk
sumber
3

Ini adalah implementasi lain, dengan jumlah objek terbatas.

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();
        }
    }
}
Peter K.
sumber