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ẽ để Pool
lớ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 size
và count
ở 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. AcquireEager
là đơ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 PreloadItems
phương thức hiển thị cuối cùng.
AcquireLazy
kiể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. AcquireLazyExpanding
sẽ 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 PreloadItems
phươ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ế Acquire
và Release
phươ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 IsDisposed
tài sản đó sẽ trở nên rõ ràng trong giây lát. Tất cả các Dispose
phươ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-finally
khố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 có 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à Foo
tài nguyên dùng một lần của chúng tôi thực hiện IFoo
và 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 Pool
cá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.