2015-01-31 16 views
13

Nói rằng tôi có lớp sau:Điều gì xảy ra với Công việc chưa bao giờ hoàn thành? Chúng có được xử lý đúng cách không?

class SomeClass 
{ 
    private TaskCompletionSource<string> _someTask; 

    public Task<string> WaitForThing() 
    { 
     _someTask = new TaskCompletionSource<string>(); 
     return _someTask.Task; 
    } 

    //Other code which calls _someTask.SetResult(..); 
} 

Sau đó, ở đâu đó, tôi gọi

//Some code.. 
await someClassInstance.WaitForThing(); 
//Some more code 

Các //Some more code sẽ không được gọi cho đến khi _someTask.SetResult(..) được gọi. Ngữ cảnh gọi điện đang chờ đợi trong bộ nhớ ở đâu đó.

Tuy nhiên, giả sử SetResult(..) không bao giờ được gọi và someClassInstance dừng tham chiếu và thu thập rác. Điều này có tạo ra rò rỉ bộ nhớ không? Hoặc không. Net tự động kỳ diệu biết ngữ cảnh gọi điện thoại cần phải được xử lý?

+0

TCS là một đối tượng bị cô lập. Khi nó không thể truy cập, nó sẽ được thu thập. Nhiệm vụ của nó không tham chiếu đến TCS để bạn có thể có một nhiệm vụ không thể hoàn thành mà không có bất kỳ TCS tương ứng nào trong bộ nhớ. Nếu nhiệm vụ đó không được chấp nhận thì nó cũng được thu thập. – usr

Trả lời

9

Cập nhật, một điểm tốt bởi @SriramSakthivel, nó quay ra tôi đã trả lời một câu hỏi rất giống nhau:

Why does GC collects my object when I have a reference to it?

Vì vậy, tôi đánh dấu này như là một cộng đồng wiki.

Tuy nhiên, giả sử SetResult (..) không bao giờ được gọi và someClassInstance dừng tham chiếu và bị thu thập rác. Điều này có tạo ra rò rỉ bộ nhớ không? Hoặc không .Net tự động biết một cách rõ ràng ngữ cảnh gọi là cần phải được xử lý?

Nếu bởi gọi-bối cảnh bạn có nghĩa là đối tượng máy nhà nước trình biên dịch tạo (đại diện cho nhà nước của phương pháp async), sau đó có, nó sẽ thực sự được hoàn thành.

Ví dụ:

static void Main(string[] args) 
{ 
    var task = TestSomethingAsync(); 
    Console.WriteLine("Press enter to GC"); 
    Console.ReadLine(); 
    GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true); 
    GC.WaitForFullGCComplete(); 
    GC.WaitForPendingFinalizers(); 
    Console.WriteLine("Press enter to exit"); 
    Console.ReadLine(); 
} 

static async Task TestSomethingAsync() 
{ 
    using (var something = new SomeDisposable()) 
    { 
     await something.WaitForThingAsync(); 
    } 
} 

class SomeDisposable : IDisposable 
{ 
    readonly TaskCompletionSource<string> _tcs = new TaskCompletionSource<string>(); 

    ~SomeDisposable() 
    { 
     Console.WriteLine("~SomeDisposable"); 
    } 

    public Task<string> WaitForThingAsync() 
    { 
     return _tcs.Task; 
    } 

    public void Dispose() 
    { 
     Console.WriteLine("SomeDisposable.Dispose"); 
     GC.SuppressFinalize(this); 
    } 
} 

Output:

 
Press enter to GC 

~SomeDisposable 
Press enter to exit 

IMO, hành vi này là hợp lý, nhưng nó còn có thể là một chút bất ngờ mà something được hoàn thành mặc dù thực tế rằng using phạm vi cho nó chưa bao giờ kết thúc (và do đó SomeDisposable.Dispose của nó chưa bao giờ được gọi) và rằng Task được trả lại bởi TestSomethingAsync vẫn còn hoạt động và được tham chiếu trong Main.

Điều này có thể dẫn đến một số lỗi không rõ ràng khi mã hóa nội dung không đồng bộ ở cấp hệ thống. Điều thực sự quan trọng là sử dụng GCHandle.Alloc(callback) trên bất kỳ cuộc gọi lại interop hệ điều hành nào không được tham chiếu bên ngoài các phương thức async. Làm GC.KeepAlive(callback) một mình ở cuối phương pháp async không hiệu quả. Tôi đã viết về điều này trong chi tiết tại đây:

Async/await, custom awaiter and garbage collector

Trên một mặt lưu ý, có một loại C máy # trạng thái: một phương thức với return yield. Điều thú vị là, cùng với IEnumerable hoặc IEnumerator, nó cũng thực hiện IDisposable. Gọi Dispose của nó sẽ bung ra bất kỳ usingfinally báo cáo (ngay cả trong trường hợp chuỗi đếm không đầy đủ):

static IEnumerator SomethingEnumerable() 
{ 
    using (var disposable = new SomeDisposable()) 
    { 
     try 
     { 
      Console.WriteLine("Step 1"); 
      yield return null; 
      Console.WriteLine("Step 2"); 
      yield return null; 
      Console.WriteLine("Step 3"); 
      yield return null; 
     } 
     finally 
     { 
      Console.WriteLine("Finally"); 
     } 
    } 
} 
// ... 
var something = SomethingEnumerable(); 
something.MoveNext(); // prints "Step 1" 
var disposable = (IDisposable)something; 
disposable.Dispose(); // prints "Finally", "SomeDisposable.Dispose" 

Không giống như này, với async phương pháp không có cách nào trực tiếp của việc kiểm soát unwiding của usingfinally.

+0

Làm cách nào để bạn biết * "đối tượng máy trạng thái do trình biên dịch tạo ra" * được bảo đảm được xử lý khi đối tượng được hoàn thành, mặc dù? –

+0

@ BlueRaja-DannyPflughoeft, ý bạn là gì dưới "được xử lý" ở đây? Đối tượng máy trạng thái không thực hiện 'IDisposable'. Nó chỉ là một 'struct' được đóng hộp, nhưng tất cả các trường của nó (như' something' ở trên) đều lấy rác và được thu thập (lưu ý cách '~ SomeDisposable' được gọi) - bởi vì cấu trúc tự nhận được GC'ed. – Noseratio

+0

@SriramSakthivel, cảm ơn vì quan điểm của bạn về một bản sao, tôi đã đánh dấu trang này là một wiki. – Noseratio

6

You should ensure your tasks are always completed.

Trong trường hợp thông thường, "Mã khác gọi SetResult" được đăng ký dưới dạng gọi lại ở đâu đó. Ví dụ: nếu sử dụng I/O chồng chéo không được quản lý thì phương thức gọi lại đó là gốc GC. Sau đó, gọi lại đó giữ lại một cách rõ ràng _someTask còn sống, giữ cho hoạt động Task của mình giữ cho người được ủy quyền giữ //Some more code còn sống.

Nếu "mã khác trong đó kêu gọi SetResult" là không (trực tiếp hoặc gián tiếp) đăng ký như một callback, sau đó tôi không nghĩ sẽ có một sự rò rỉ. Lưu ý rằng đây không phải là trường hợp sử dụng được hỗ trợ, do đó, điều này không được đảm bảo. Nhưng tôi đã tạo ra một bài kiểm tra hồ sơ bộ nhớ bằng cách sử dụng mã trong câu hỏi của bạn và nó không xuất hiện để rò rỉ.

+0

Vì vậy, nó phụ thuộc vào nơi "mã khác" là? Trường hợp sử dụng của tôi là 'WaitForThing()' đợi đầu vào của người dùng từ một người dùng ở đầu kia của ứng dụng khách IM * (và được gọi từ đối tượng mà đại diện cho một "người bạn") * và trả lại nó. Tuy nhiên, nếu người dùng đó bị xóa như một người bạn, thì đầu vào đó không bao giờ có thể xảy ra - không có cách nào để tôi "hoàn thành" nó. –

+1

@ BlueRaja-DannyPflughoeft: Tôi khuyên bạn nên hoàn tất tác vụ bị hủy ('SetCanceled') khi người dùng bị xóa làm bạn. –

Các vấn đề liên quan