คำถามนี้มีเล่ห์เหลี่ยมกว่าที่คาดไว้เล็กน้อยเนื่องจากมีหลายสิ่งที่ไม่ทราบ: พฤติกรรมของทรัพยากรที่ถูกพูลอายุการใช้งานที่คาดหวัง / ที่ต้องการของวัตถุเหตุผลที่แท้จริงที่ต้องใช้พูล ฯลฯ โดยทั่วไปพูลมีวัตถุประสงค์พิเศษ - เธรด พูล, พูลการเชื่อมต่อ, ฯลฯ - เพราะมันง่ายกว่าในการออปติไมซ์เมื่อคุณรู้ว่าทรัพยากรทำอะไรและที่สำคัญกว่านั้นมีการควบคุมการใช้ทรัพยากร
เนื่องจากมันไม่ง่ายเลยสิ่งที่ฉันพยายามทำคือเสนอวิธีการที่ยืดหยุ่นพอสมควรที่คุณสามารถทดลองและดูว่าอะไรดีที่สุด ขออภัยล่วงหน้าสำหรับการโพสต์ที่ยาวนาน แต่มีพื้นที่มากมายที่จะครอบคลุมเมื่อมันมาถึงการใช้ทรัพยากรที่มีวัตถุประสงค์ทั่วไปที่ดี และฉันแค่เกาที่ผิวเท่านั้น
กลุ่มวัตถุประสงค์ทั่วไปจะต้องมี "การตั้งค่า" หลักบางประการซึ่งรวมถึง:
- กลยุทธ์การโหลดทรัพยากร - กระตือรือร้นหรือขี้เกียจ
- กลไกการโหลดทรัพยากร- วิธีสร้างหนึ่งจริง
- กลยุทธ์การเข้าถึง - คุณพูดถึง "round robin" ซึ่งไม่ตรงไปตรงมาเท่าที่ฟัง การนำไปใช้นี้สามารถใช้บัฟเฟอร์วงกลมซึ่งคล้ายกันแต่ไม่สมบูรณ์แบบเนื่องจากพูลไม่สามารถควบคุมได้เมื่อทรัพยากรถูกเรียกคืนจริง ตัวเลือกอื่น ๆ คือ FIFO และ LIFO; FIFO จะมีรูปแบบการเข้าถึงแบบสุ่มมากขึ้น แต่ LIFO ทำให้การใช้กลยุทธ์การทำให้เป็นอิสระอย่างน้อยล่าสุดใช้ง่ายขึ้น (ซึ่งคุณบอกว่าอยู่นอกขอบเขต แต่ก็คุ้มค่าที่จะกล่าวถึง)
สำหรับกลไกการโหลดทรัพยากร. NET ได้มอบสิ่งที่เป็นนามธรรมให้กับเราอย่างสมบูรณ์
private Func<Pool<T>, T> factory;
ผ่านสิ่งนี้ผ่าน Constructor ของ Pool และเรากำลังทำสิ่งนั้น การใช้ประเภทสามัญที่มี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 มีเครื่องมือที่ดีอย่างสมบูรณ์สำหรับสิ่งนั้นเรียกว่าเซมาฟอร์และได้รับการออกแบบมาโดยเฉพาะ จำนวนเธรดที่เข้าถึงทรัพยากร (ในกรณีนี้ "ทรัพยากร" คือที่เก็บรายการภายใน) เนื่องจากเราไม่ได้ใช้คิวผู้ผลิต / ผู้บริโภคแบบเต็มนี่จึงเพียงพอสำหรับความต้องการของเรา
ตัวสร้างมีลักษณะดังนี้:
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
วิธีจริงๆไม่เป็นทิ้งรายการ pooled 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
(เราสามารถทำได้ด้วย Dynamic Proxy library เช่น 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
(เมื่อเทียบกับรหัสที่สร้างมัน) ไม่ต้องการจริงที่จะตระหนักถึงสระว่ายน้ำ คุณยังสามารถฉีด IFoo
วัตถุใช้ห้องสมุด DI ที่คุณชื่นชอบและPool<T>
เป็นผู้ให้บริการ / โรงงาน
ฉันใส่รหัสที่สมบูรณ์ใน PasteBinเพื่อความเพลิดเพลินในการคัดลอกและวางของคุณ นอกจากนี้ยังมีโปรแกรมทดสอบสั้น ๆ ที่คุณสามารถใช้เล่นกับโหมดการโหลด / การเข้าถึงและเงื่อนไขแบบมัลติเธรดที่แตกต่างกัน
แจ้งให้เราทราบหากคุณมีคำถามหรือข้อสงสัยเกี่ยวกับเรื่องนี้