Thông tin tôi đưa ra ở đây không phải là mới, tôi chỉ bổ sung cái này cho đầy đủ.
Ý tưởng của mã này khá đơn giản:
- Các đối tượng cần một ID duy nhất, ID này không có theo mặc định. Thay vào đó, chúng tôi phải dựa vào điều tốt nhất tiếp theo, đó là cung cấp
RuntimeHelpers.GetHashCode
cho chúng tôi một loại ID duy nhất
- Để kiểm tra tính duy nhất, điều này có nghĩa là chúng ta cần sử dụng
object.ReferenceEquals
- Tuy nhiên, chúng tôi vẫn muốn có một ID duy nhất, vì vậy tôi đã thêm một ID,
GUID
theo định nghĩa là duy nhất.
- Bởi vì tôi không thích khóa tất cả mọi thứ nếu tôi không cần thiết, tôi không sử dụng
ConditionalWeakTable
.
Kết hợp, điều đó sẽ cung cấp cho bạn mã sau:
public class UniqueIdMapper
{
private class ObjectEqualityComparer : IEqualityComparer<object>
{
public bool Equals(object x, object y)
{
return object.ReferenceEquals(x, y);
}
public int GetHashCode(object obj)
{
return RuntimeHelpers.GetHashCode(obj);
}
}
private Dictionary<object, Guid> dict = new Dictionary<object, Guid>(new ObjectEqualityComparer());
public Guid GetUniqueId(object o)
{
Guid id;
if (!dict.TryGetValue(o, out id))
{
id = Guid.NewGuid();
dict.Add(o, id);
}
return id;
}
}
Để sử dụng nó, hãy tạo một thể hiện của UniqueIdMapper
và sử dụng GUID mà nó trả về cho các đối tượng.
Phụ lục
Vì vậy, có một chút nữa đang diễn ra ở đây; hãy để tôi viết một chút về ConditionalWeakTable
.
ConditionalWeakTable
làm một vài điều. Điều quan trọng nhất là nó không quan tâm đến bộ thu gom rác, đó là: các đối tượng mà bạn tham chiếu trong bảng này sẽ được thu thập bất kể. Nếu bạn tra cứu một đối tượng, về cơ bản nó hoạt động giống như từ điển ở trên.
Tò mò không? Rốt cuộc, khi một đối tượng đang được GC thu thập, nó sẽ kiểm tra xem có tham chiếu đến đối tượng hay không và nếu có, nó sẽ thu thập chúng. Vì vậy, nếu có một đối tượng từ ConditionalWeakTable
, tại sao đối tượng được tham chiếu sẽ được thu thập sau đó?
ConditionalWeakTable
sử dụng một thủ thuật nhỏ mà một số cấu trúc .NET khác cũng sử dụng: thay vì lưu trữ một tham chiếu đến đối tượng, nó thực sự lưu trữ một IntPtr. Vì đó không phải là tham chiếu thực, đối tượng có thể được thu thập.
Vì vậy, tại thời điểm này, có 2 vấn đề cần giải quyết. Đầu tiên, các đối tượng có thể được di chuyển trên heap, vậy chúng ta sẽ sử dụng IntPtr là gì? Và thứ hai, làm sao chúng ta biết rằng các đối tượng có một tham chiếu đang hoạt động?
- Đối tượng có thể được ghim trên heap và con trỏ thực của nó có thể được lưu trữ. Khi GC chạm vào đối tượng để loại bỏ, nó sẽ mở khóa và thu thập nó. Tuy nhiên, điều đó có nghĩa là chúng ta nhận được một tài nguyên được ghim, đây không phải là ý kiến hay nếu bạn có nhiều đối tượng (do vấn đề phân mảnh bộ nhớ). Đây có lẽ không phải là cách nó hoạt động.
- Khi GC di chuyển một đối tượng, nó sẽ gọi lại, sau đó có thể cập nhật các tham chiếu. Đây có thể là cách nó được triển khai dựa trên các cuộc gọi bên ngoài
DependentHandle
- nhưng tôi tin rằng nó phức tạp hơn một chút.
- Không phải con trỏ đến chính đối tượng, nhưng một con trỏ trong danh sách tất cả các đối tượng từ GC được lưu trữ. IntPtr là một chỉ mục hoặc một con trỏ trong danh sách này. Danh sách chỉ thay đổi khi một đối tượng thay đổi các thế hệ, lúc đó một lệnh gọi lại đơn giản có thể cập nhật các con trỏ. Nếu bạn nhớ cách hoạt động của Mark & Sweep, điều này có ý nghĩa hơn. Không có ghim và loại bỏ như trước đây. Tôi tin rằng đây là cách nó hoạt động
DependentHandle
.
Giải pháp cuối cùng này yêu cầu thời gian chạy không sử dụng lại các nhóm danh sách cho đến khi chúng được giải phóng rõ ràng và nó cũng yêu cầu tất cả các đối tượng được truy xuất bằng một lệnh gọi đến thời gian chạy.
Nếu giả sử họ sử dụng giải pháp này, chúng ta cũng có thể giải quyết vấn đề thứ hai. Thuật toán Mark & Sweep theo dõi đối tượng nào đã được thu thập; ngay sau khi nó đã được thu thập, chúng tôi biết vào thời điểm này. Khi đối tượng kiểm tra xem đối tượng có ở đó hay không, nó sẽ gọi là 'Miễn phí', sẽ loại bỏ con trỏ và mục nhập danh sách. Đối tượng đã thực sự biến mất.
Một điều quan trọng cần lưu ý tại thời điểm này là mọi thứ trở nên sai lầm khủng khiếp nếu ConditionalWeakTable
được cập nhật trong nhiều chuỗi và nếu nó không an toàn cho chuỗi. Kết quả là sẽ bị rò rỉ bộ nhớ. Đây là lý do tại sao tất cả các cuộc gọi đến ConditionalWeakTable
đều thực hiện một 'khóa' đơn giản để đảm bảo điều này không xảy ra.
Một điều cần lưu ý nữa là việc dọn dẹp các mục nhập phải diễn ra một lần. Trong khi các đối tượng thực tế sẽ được GC làm sạch, các mục nhập thì không. Đây là lý do tại sao ConditionalWeakTable
chỉ phát triển về kích thước. Khi nó đạt đến một giới hạn nhất định (được xác định bởi cơ hội va chạm trong hàm băm), nó sẽ kích hoạt a Resize
, kiểm tra xem các đối tượng có phải được dọn dẹp hay không - nếu có, free
được gọi trong quy trình GC, loại bỏ phần IntPtr
xử lý.
Tôi tin rằng đây cũng là lý do tại sao DependentHandle
không được tiếp xúc trực tiếp - bạn không muốn làm rối tung mọi thứ và kết quả là bị rò rỉ bộ nhớ. Điều tốt nhất tiếp theo cho điều đó là một WeakReference
(cũng lưu trữ một IntPtr
thay vì một đối tượng) - nhưng tiếc là không bao gồm khía cạnh 'phụ thuộc'.
Những gì còn lại là để bạn đùa giỡn với cơ khí, để bạn có thể thấy sự phụ thuộc trong hành động. Hãy đảm bảo bắt đầu nó nhiều lần và xem kết quả:
class DependentObject
{
public class MyKey : IDisposable
{
public MyKey(bool iskey)
{
this.iskey = iskey;
}
private bool disposed = false;
private bool iskey;
public void Dispose()
{
if (!disposed)
{
disposed = true;
Console.WriteLine("Cleanup {0}", iskey);
}
}
~MyKey()
{
Dispose();
}
}
static void Main(string[] args)
{
var dep = new MyKey(true); // also try passing this to cwt.Add
ConditionalWeakTable<MyKey, MyKey> cwt = new ConditionalWeakTable<MyKey, MyKey>();
cwt.Add(new MyKey(true), dep); // try doing this 5 times f.ex.
GC.Collect(GC.MaxGeneration);
GC.WaitForFullGCComplete();
Console.WriteLine("Wait");
Console.ReadLine(); // Put a breakpoint here and inspect cwt to see that the IntPtr is still there
}