यह सवाल कई अज्ञात लोगों के कारण होने वाली अपेक्षा की तुलना में थोड़ा पेचीदा है: संसाधन का व्यवहार पूल किया जा रहा है, वस्तुओं की अपेक्षित / अपेक्षित जीवनकाल, वास्तविक कारण जो पूल की आवश्यकता है, आदि। आमतौर पर पूल विशेष उद्देश्य-थ्रेड होते हैं। पूल, कनेक्शन पूल, इत्यादि - क्योंकि जब आप जानते हैं कि संसाधन क्या करता है और अधिक महत्वपूर्ण बात यह है कि उस संसाधन को कैसे लागू किया जाता है पर नियंत्रण करना आसान है।
चूंकि यह इतना आसान नहीं है, मैंने जो करने की कोशिश की है वह काफी लचीला दृष्टिकोण है जिसे आप प्रयोग कर सकते हैं और देख सकते हैं कि सबसे अच्छा काम क्या है। अग्रिम में लंबी पोस्ट के लिए क्षमा याचना, लेकिन एक सामान्य सामान्य प्रयोजन संसाधन पूल को लागू करने के लिए कवर करने के लिए बहुत कुछ जमीन है। और मैं वास्तव में केवल सतह को खरोंच रहा हूं।
एक सामान्य-उद्देश्य वाले पूल में कुछ मुख्य "सेटिंग" होनी चाहिए, जिनमें शामिल हैं:
- संसाधन लोड करने की रणनीति - उत्सुक या आलसी;
- संसाधन लोडिंग तंत्र - वास्तव में एक का निर्माण कैसे करें;
- पहुंच की रणनीति - आप "राउंड रॉबिन" का उल्लेख करते हैं जो उतना सरल नहीं है जितना लगता है; यह कार्यान्वयन एक परिपत्र बफर का उपयोग कर सकता है जो समान है , लेकिन सही नहीं है, क्योंकि संसाधनों पर वास्तव में पुनः प्राप्त होने पर पूल का कोई नियंत्रण नहीं है। अन्य विकल्प FIFO और LIFO हैं; FIFO में एक यादृच्छिक-अभिगम पैटर्न अधिक होगा, लेकिन LIFO इसे कम से कम हाल ही में उपयोग की जाने वाली मुक्त रणनीति को लागू करना आसान बनाता है (जो आपने कहा था कि यह दायरे से बाहर है, लेकिन यह अभी भी ध्यान देने योग्य है)।
संसाधन लोडिंग तंत्र के लिए, .NET पहले से ही हमें एक साफ अमूर्त - प्रतिनिधि देता है।
private Func<Pool<T>, T> factory;
पूल के कंस्ट्रक्टर के माध्यम से इसे पास करें और हम उसी के साथ काम कर रहे हैं। एक new()
बाधा के साथ एक सामान्य प्रकार का उपयोग करना भी काम करता है, लेकिन यह अधिक लचीला है।
अन्य दो मापदंडों में से, एक्सेस रणनीति अधिक जटिल जानवर है, इसलिए मेरा दृष्टिकोण एक वंशानुक्रम (इंटरफ़ेस) आधारित दृष्टिकोण का उपयोग करना था:
public class Pool<T> : IDisposable
{
// Other code - we'll come back to this
interface IItemStore
{
T Fetch();
void Store(T item);
int Count { get; }
}
}
यहां अवधारणा सरल है - हम सार्वजनिक Pool
वर्ग को थ्रेड-सुरक्षा जैसे सामान्य मुद्दों को संभालने देंगे , लेकिन प्रत्येक एक्सेस पैटर्न के लिए एक अलग "आइटम स्टोर" का उपयोग करें। LIFO को एक स्टैक द्वारा आसानी से दर्शाया जाता है, FIFO एक कतार है, और मैंने List<T>
एक राउंड-रॉबिन एक्सेस पैटर्न का अनुमान लगाने के लिए एक और इंडेक्स पॉइंटर का उपयोग करके एक बहुत-अनुकूलित-लेकिन-शायद-पर्याप्त-पर्याप्त परिपत्र बफर कार्यान्वयन का उपयोग नहीं किया है ।
नीचे दी गई सभी कक्षाएं आंतरिक कक्षाएं हैं Pool<T>
- यह एक शैली का विकल्प था, लेकिन चूंकि ये वास्तव में बाहर से उपयोग करने के लिए नहीं हैं Pool
, इसलिए यह सबसे अधिक समझ में आता है।
class QueueStore : Queue<T>, IItemStore
{
public QueueStore(int capacity) : base(capacity)
{
}
public T Fetch()
{
return Dequeue();
}
public void Store(T item)
{
Enqueue(item);
}
}
class StackStore : Stack<T>, IItemStore
{
public StackStore(int capacity) : base(capacity)
{
}
public T Fetch()
{
return Pop();
}
public void Store(T item)
{
Push(item);
}
}
ये स्पष्ट हैं - स्टैक और कतार। मुझे नहीं लगता कि वे वास्तव में बहुत स्पष्टीकरण देते हैं। परिपत्र बफर थोड़ा अधिक जटिल है:
class CircularStore : IItemStore
{
private List<Slot> slots;
private int freeSlotCount;
private int position = -1;
public CircularStore(int capacity)
{
slots = new List<Slot>(capacity);
}
public T Fetch()
{
if (Count == 0)
throw new InvalidOperationException("The buffer is empty.");
int startPosition = position;
do
{
Advance();
Slot slot = slots[position];
if (!slot.IsInUse)
{
slot.IsInUse = true;
--freeSlotCount;
return slot.Item;
}
} while (startPosition != position);
throw new InvalidOperationException("No free slots.");
}
public void Store(T item)
{
Slot slot = slots.Find(s => object.Equals(s.Item, item));
if (slot == null)
{
slot = new Slot(item);
slots.Add(slot);
}
slot.IsInUse = false;
++freeSlotCount;
}
public int Count
{
get { return freeSlotCount; }
}
private void Advance()
{
position = (position + 1) % slots.Count;
}
class Slot
{
public Slot(T item)
{
this.Item = item;
}
public T Item { get; private set; }
public bool IsInUse { get; set; }
}
}
मैं कई अलग-अलग तरीकों को चुन सकता था, लेकिन लब्बोलुआब यह है कि संसाधनों को उसी क्रम में एक्सेस किया जाना चाहिए जो वे बनाए गए थे, जिसका अर्थ है कि हमें उनके संदर्भ बनाए रखना है लेकिन उन्हें "उपयोग में" (या नहीं) के रूप में चिह्नित करना होगा )। सबसे खराब स्थिति में, केवल एक स्लॉट कभी भी उपलब्ध होता है, और यह प्रत्येक भ्रूण के लिए बफर का पूर्ण पुनरावृत्ति लेता है। यह बुरा है यदि आपके पास सैकड़ों संसाधन हैं, जो प्रति सेकंड कई बार प्राप्त होते हैं और उन्हें जारी करते हैं; वास्तव में 5-10 वस्तुओं के पूल के लिए एक मुद्दा नहीं है, और विशिष्ट मामले में, जहां संसाधनों का हल्के ढंग से उपयोग किया जाता है, इसे केवल एक या दो स्लॉट को आगे बढ़ाना होगा।
याद रखें, ये कक्षाएं निजी आंतरिक कक्षाएं हैं - यही कारण है कि उन्हें पूरी तरह से त्रुटि-जाँच की आवश्यकता नहीं है, पूल स्वयं उन तक पहुंच को प्रतिबंधित करता है।
एक एन्यूमरेशन और एक फैक्ट्री विधि में फेंकें और हम इस भाग के साथ काम करें:
// Outside the pool
public enum AccessMode { FIFO, LIFO, Circular };
private IItemStore itemStore;
// Inside the Pool
private IItemStore CreateItemStore(AccessMode mode, int capacity)
{
switch (mode)
{
case AccessMode.FIFO:
return new QueueStore(capacity);
case AccessMode.LIFO:
return new StackStore(capacity);
default:
Debug.Assert(mode == AccessMode.Circular,
"Invalid AccessMode in CreateItemStore");
return new CircularStore(capacity);
}
}
समाधान करने के लिए अगली समस्या लोडिंग रणनीति है। मैंने तीन प्रकारों को परिभाषित किया है:
public enum LoadingMode { Eager, Lazy, LazyExpanding };
पहले दो को आत्म-व्याख्यात्मक होना चाहिए; तीसरा एक हाइब्रिड की तरह है, यह संसाधनों को आलसी बनाता है लेकिन वास्तव में पूल के पूर्ण होने तक किसी भी संसाधन का फिर से उपयोग शुरू नहीं करता है। यदि आप चाहते हैं कि पूल पूरा भरा हुआ हो (जो आपको ऐसा लगता है) तो यह एक अच्छा व्यापार-बंद होगा, लेकिन वास्तव में पहली पहुंच (स्टार्टअप समय में सुधार करने के लिए) तक उन्हें बनाने का खर्च स्थगित करना चाहते हैं।
लोडिंग विधियां वास्तव में बहुत जटिल नहीं हैं, अब हमारे पास आइटम-स्टोर अमूर्त है:
private int size;
private int count;
private T AcquireEager()
{
lock (itemStore)
{
return itemStore.Fetch();
}
}
private T AcquireLazy()
{
lock (itemStore)
{
if (itemStore.Count > 0)
{
return itemStore.Fetch();
}
}
Interlocked.Increment(ref count);
return factory(this);
}
private T AcquireLazyExpanding()
{
bool shouldExpand = false;
if (count < size)
{
int newCount = Interlocked.Increment(ref count);
if (newCount <= size)
{
shouldExpand = true;
}
else
{
// Another thread took the last spot - use the store instead
Interlocked.Decrement(ref count);
}
}
if (shouldExpand)
{
return factory(this);
}
else
{
lock (itemStore)
{
return itemStore.Fetch();
}
}
}
private void PreloadItems()
{
for (int i = 0; i < size; i++)
{
T item = factory(this);
itemStore.Store(item);
}
count = size;
}
size
और count
खाने के ऊपर और पूल का अधिकतम आकार पूल (लेकिन जरूरी नहीं कि के स्वामित्व वाले संसाधनों की कुल संख्या का उल्लेख उपलब्ध क्रमशः),। AcquireEager
सबसे सरल है, यह मानता है कि एक आइटम पहले से ही स्टोर में है - ये आइटम निर्माण में पहले से लोड किए जाएंगे, अर्थात PreloadItems
पिछले अपडेट की गई विधि में।
AcquireLazy
यह देखने के लिए कि क्या पूल में मुफ्त आइटम हैं, और यदि नहीं, तो यह एक नया निर्माण करता है। AcquireLazyExpanding
जब तक पूल अपने लक्ष्य आकार तक नहीं पहुँच जाता है तब तक एक नया संसाधन तैयार करेगा। मैंने इसे लॉक करने को कम से कम करने के लिए इसे अनुकूलित करने की कोशिश की है, और मुझे आशा है कि मैंने कोई गलती नहीं की है (मैंने इसे बहु-थ्रेडेड परिस्थितियों में परीक्षण किया है, लेकिन स्पष्ट रूप से नहीं।
आप सोच रहे होंगे कि इनमें से कोई भी तरीका यह देखने के लिए जाँचने में परेशान नहीं करता कि दुकान अधिकतम आकार तक पहुँची है या नहीं। मैं एक पल में मिल जाएगा।
अब पूल के लिए ही। यहां निजी डेटा का पूरा सेट दिया गया है, जिनमें से कुछ पहले ही दिखाए जा चुके हैं:
private bool isDisposed;
private Func<Pool<T>, T> factory;
private LoadingMode loadingMode;
private IItemStore itemStore;
private int size;
private int count;
private Semaphore sync;
अंतिम पैराग्राफ में मैंने जो सवाल किया था, उसका उत्तर देते हुए - हम यह सुनिश्चित करना चाहते हैं कि हम बनाए गए संसाधनों की कुल संख्या को कैसे सीमित करें - यह पता चलता है कि .NET के पास पहले से ही इसके लिए एक अच्छा उपकरण है, इसे Semaphore कहा जाता है और इसे विशेष रूप से एक निश्चित अनुमति देने के लिए डिज़ाइन किया गया है। किसी संसाधन तक थ्रेड की संख्या (इस मामले में "संसाधन" आंतरिक आइटम स्टोर है)। चूंकि हम एक पूर्ण निर्माता / उपभोक्ता कतार को लागू नहीं कर रहे हैं, यह हमारी आवश्यकताओं के लिए पूरी तरह से पर्याप्त है।
कंस्ट्रक्टर इस तरह दिखता है:
public Pool(int size, Func<Pool<T>, T> factory,
LoadingMode loadingMode, AccessMode accessMode)
{
if (size <= 0)
throw new ArgumentOutOfRangeException("size", size,
"Argument 'size' must be greater than zero.");
if (factory == null)
throw new ArgumentNullException("factory");
this.size = size;
this.factory = factory;
sync = new Semaphore(size, size);
this.loadingMode = loadingMode;
this.itemStore = CreateItemStore(accessMode, size);
if (loadingMode == LoadingMode.Eager)
{
PreloadItems();
}
}
यहां कोई आश्चर्य नहीं होना चाहिए। केवल ध्यान देने योग्य बात यह है कि उत्सुक लोडिंग के लिए विशेष आवरण है, PreloadItems
पहले से दिखाए गए तरीके का उपयोग करके ।
चूंकि अब तक लगभग सब कुछ साफ-सुथरा हो चुका है, वास्तविक Acquire
और Release
विधियां वास्तव में बहुत सीधी हैं:
public T Acquire()
{
sync.WaitOne();
switch (loadingMode)
{
case LoadingMode.Eager:
return AcquireEager();
case LoadingMode.Lazy:
return AcquireLazy();
default:
Debug.Assert(loadingMode == LoadingMode.LazyExpanding,
"Unknown LoadingMode encountered in Acquire method.");
return AcquireLazyExpanding();
}
}
public void Release(T item)
{
lock (itemStore)
{
itemStore.Store(item);
}
sync.Release();
}
जैसा कि पहले बताया गया है, हम Semaphore
धार्मिक रूप से आइटम स्टोर की स्थिति की जांच करने के बजाय संगामिति को नियंत्रित करने के लिए उपयोग कर रहे हैं। जब तक अधिग्रहित वस्तुओं को सही ढंग से जारी नहीं किया जाता है, तब तक चिंता की कोई बात नहीं है।
पिछले नहीं बल्कि कम से कम, वहाँ सफाई है:
public void Dispose()
{
if (isDisposed)
{
return;
}
isDisposed = true;
if (typeof(IDisposable).IsAssignableFrom(typeof(T)))
{
lock (itemStore)
{
while (itemStore.Count > 0)
{
IDisposable disposable = (IDisposable)itemStore.Fetch();
disposable.Dispose();
}
}
}
sync.Close();
}
public bool IsDisposed
{
get { return isDisposed; }
}
उस IsDisposed
संपत्ति का उद्देश्य एक पल में स्पष्ट हो जाएगा। Dispose
यदि वे लागू करते हैं तो सभी मुख्य विधि वास्तव में वास्तविक पूलित वस्तुओं का निपटान करती है IDisposable
।
अब आप मूल रूप से इस का उपयोग कर सकते हैं-एक try-finally
ब्लॉक के साथ , लेकिन मैं उस वाक्य रचना का शौकीन नहीं हूं, क्योंकि यदि आप कक्षाओं और विधियों के बीच जमा संसाधनों से गुजरना शुरू करते हैं तो यह बहुत भ्रमित करने वाला है। यह संभव है कि मुख्य वर्ग जो संसाधन का उपयोग करता है, उसके पास पूल का संदर्भ भी नहीं है । यह वास्तव में काफी गड़बड़ हो जाता है, इसलिए एक बेहतर दृष्टिकोण "स्मार्ट" पूल वाली वस्तु बनाना है।
मान लें कि हम निम्नलिखित सरल अंतरफलक / वर्ग से शुरू करते हैं:
public interface IFoo : IDisposable
{
void Test();
}
public class Foo : IFoo
{
private static int count = 0;
private int num;
public Foo()
{
num = Interlocked.Increment(ref count);
}
public void Dispose()
{
Console.WriteLine("Goodbye from Foo #{0}", num);
}
public void Test()
{
Console.WriteLine("Hello from Foo #{0}", num);
}
}
यहाँ हमारा दिखावा प्रयोज्य Foo
संसाधन है जो IFoo
अद्वितीय पहचान उत्पन्न करने के लिए कुछ बॉयलरप्लेट कोड लागू करता है और है। हम जो करते हैं वह एक और विशेष, पूलित वस्तु बनाने के लिए होता है:
public class PooledFoo : IFoo
{
private Foo internalFoo;
private Pool<IFoo> pool;
public PooledFoo(Pool<IFoo> pool)
{
if (pool == null)
throw new ArgumentNullException("pool");
this.pool = pool;
this.internalFoo = new Foo();
}
public void Dispose()
{
if (pool.IsDisposed)
{
internalFoo.Dispose();
}
else
{
pool.Release(this);
}
}
public void Test()
{
internalFoo.Test();
}
}
यह सिर्फ "वास्तविक" तरीकों के सभी को अपने भीतर तक पहुँचाता है IFoo
(हम इसे कैसल की तरह एक डायनेमिक प्रॉक्सी लाइब्रेरी के साथ कर सकते हैं, लेकिन मैं इसमें नहीं मिलेगा)। यह एक संदर्भ भी रखता है Pool
जो इसे बनाता है, ताकि जब हम Dispose
इस वस्तु को बनाते हैं, तो यह स्वतः ही पूल में वापस आ जाता है। सिवाय जब पूल को पहले ही निपटा दिया गया है - इसका मतलब है कि हम "सफाई" मोड में हैं और इस मामले में यह वास्तव में इसके बजाय आंतरिक संसाधन को साफ करता है ।
उपरोक्त दृष्टिकोण का उपयोग करते हुए, हमें इस तरह कोड लिखना होगा:
// Create the pool early
Pool<IFoo> pool = new Pool<IFoo>(PoolSize, p => new PooledFoo(p),
LoadingMode.Lazy, AccessMode.Circular);
// Sometime later on...
using (IFoo foo = pool.Acquire())
{
foo.Test();
}
यह करने में सक्षम होने के लिए एक बहुत अच्छी बात है। इसका मतलब है कि जो कोड का उपयोग करता हैIFoo
(कोड के विपरीत जो इसे बनाता है) वास्तव में पूल के बारे में पता करने की आवश्यकता नहीं है। तुम भी अपने पसंदीदा DI पुस्तकालय और प्रदाता / कारखाने के रूप में वस्तुओं का इंजेक्शन लगा सकते हैं ।IFoo
Pool<T>
मैंने लगा दिया है आपकी कॉपी-और- पेस्टिंग आनंद के लिए पूरा कोड पेस्टबिन पर । एक छोटा परीक्षण कार्यक्रम भी है जिसका उपयोग आप विभिन्न लोडिंग / एक्सेस मोड और मल्टीथ्रेड स्थितियों के साथ खेलने के लिए कर सकते हैं, अपने आप को संतुष्ट करने के लिए कि यह थ्रेड-सुरक्षित है और छोटी गाड़ी नहीं है।
अगर आपको इस बारे में कोई सवाल या चिंता है तो मुझे बताएं।