2017-02-03 18 views
13

Tôi vừa để ý một điều gì đó thật kỳ lạ liên quan đến việc thu gom rác thải.Phương pháp thu gom rác không đồng bộ

Phương pháp WeakRef thu thập đối tượng như mong đợi trong khi phương pháp async báo cáo rằng đối tượng vẫn còn sống mặc dù chúng tôi đã buộc phải thu thập rác. Bất kỳ ý tưởng tại sao?

class Program 
{ 
    static void Main(string[] args) 
    { 
     WeakRef(); 
     WeakRefAsync().Wait(); 
    } 

    private static void WeakRef() 
    { 
     var foo = new Foo(); 
     WeakReference fooRef = new WeakReference(foo); 
     foo = null; 
     GC.Collect(); 
     Debug.Assert(!fooRef.IsAlive); 
    } 

    private static async Task WeakRefAsync() 
    { 
     var foo = new Foo(); 
     WeakReference fooRef = new WeakReference(foo); 
     foo = null; 
     GC.Collect(); 
     Debug.Assert(!fooRef.IsAlive); 
    } 
} 


public class Foo 
{ 

} 
+0

Thật kỳ lạ nếu bạn chờ đợi bộ sưu tập rác nó được thu thập 'await System.Threading.Tasks.Task.Run (() => GC.Collect());' – Equalsk

+0

Đầu tiên nghĩ: có thể vì 'foo' và' fooRef 'trong phương thức 'async' trở thành thuộc tính của lớp máy trạng thái do trình biên dịch tạo ra. Nhưng tôi đã thử nó bằng cách gói các biến này trong một lớp khác và chúng được thu thập .... có cách nào để hỏi gc ở đâu/ai vẫn đang tham chiếu cá thể 'Foo'? –

+0

IMHO, phương thức của bạn được dịch sang một lớp bởi trình biên dịch và trình biên dịch quyết định rằng 'foo' là một thành viên của lớp đó. Vì vậy, miễn là đối tượng đại diện cho phương thức của bạn còn sống thì các thành viên của nó sẽ không là Garbage Collected –

Trả lời

8

Phương pháp WeakRef thu thập các đối tượng như mong đợi

Không có lý do để hy vọng rằng. Đang cố gắng trong Linqpad, nó không xảy ra trong một bản dựng gỡ lỗi, ví dụ, mặc dù các bộ sưu tập hợp lệ khác của cả bản dựng gỡ lỗi và bản phát hành có thể có một trong hai hành vi.

Giữa trình biên dịch và jitter, chúng được tự do tối ưu hóa phép gán rỗng (không có gì sử dụng foo sau khi nó, sau khi tất cả) trong trường hợp này GC vẫn có thể xem chuỗi có tham chiếu đến đối tượng chứ không phải thu thập nó. Ngược lại, nếu không có sự phân công nào của foo = null, chúng sẽ tự do nhận ra rằng foo không được sử dụng nữa và tái sử dụng bộ nhớ hoặc đăng ký giữ nó để giữ fooRef (hoặc thực sự cho một cái gì đó khác) và thu thập foo. Vì vậy, vì cả hai có và không có foo = null, hợp lệ để GC xem foo là bắt nguồn từ hoặc không bắt nguồn từ, chúng ta có thể mong đợi một cách hợp lý một trong hai hành vi.

Tuy nhiên, hành vi nhìn thấy là kỳ vọng kỳ vọng về điều gì sẽ có thể là xảy ra, nhưng điều đó không được đảm bảo đáng để chỉ ra.

Được rồi, sang một bên, hãy xem những gì thực sự xảy ra ở đây.

Máy trạng thái được sản xuất theo phương pháp async là cấu trúc với các trường tương ứng với người dân địa phương trong nguồn.

Vì vậy, mã:

var foo = new Foo(); 
WeakReference fooRef = new WeakReference(foo); 
foo = null; 
GC.Collect(); 

Là một chút như:

this.foo = new Foo(); 
this.fooRef = new WeakReference(foo); 
this.foo = null; 
GC.Collect(); 

Nhưng lĩnh vực truy cập luôn có một cái gì đó xảy ra tại địa phương. Vì vậy, trong vấn đề đó nó gần như:

var temp0 = new Foo(); 
this.foo = temp0; 
var temp1 = new WeakReference(foo); 
this.fooRef = temp1; 
var temp2 = null; 
this.foo = temp2; 
GC.Collect(); 

temp0 chưa được nulled, vì vậy GC tìm Foo như bắt nguồn từ.

Hai biến thể thú vị của mã của bạn là:

var foo = new Foo(); 
WeakReference fooRef = new WeakReference(foo); 
foo = null; 
await Task.Delay(0); 
GC.Collect(); 

Và:

var foo = new Foo(); 
WeakReference fooRef = new WeakReference(foo); 
foo = null; 
await Task.Delay(1); 
GC.Collect(); 

Khi tôi chạy nó (một lần nữa, sự khác biệt hợp lý trong cách bộ nhớ/đăng ký cho người dân địa phương được xử lý có thể dẫn đến trong các kết quả khác nhau) đầu tiên có cùng một hành vi của ví dụ của bạn, bởi vì trong khi nó gọi vào một phương thức Task và phương thức await của nó, phương thức đó trả về một nhiệm vụ hoàn thành để await ngay lập tức di chuyển vào n điều ext trong cùng một cuộc gọi phương thức cơ bản, là GC.Collect().

Cách thứ hai có hành vi nhìn thấy số Foo được thu thập, vì số await trả về tại thời điểm đó và sau đó máy trạng thái có phương thức MoveNext() được gọi lại khoảng một phần nghìn giây sau đó. Vì đây là một cuộc gọi mới đối với phương pháp hậu trường, không có tham chiếu cục bộ tới Foo để GC thực sự có thể thu thập nó.

Ngẫu nhiên, cũng có thể một ngày trình biên dịch sẽ không tạo ra các trường cho những người dân địa phương không sống ở các ranh giới await, đó sẽ là một tối ưu hóa. Nếu điều đó xảy ra thì hai phương pháp của bạn sẽ trở nên tương tự hơn nhiều trong hành vi cơ bản và do đó có nhiều khả năng tương tự trong hành vi được quan sát.