Việc không gọi Dispose () trên một đối tượng TPL Task có được coi là chấp nhận được không?


123

Tôi muốn kích hoạt một tác vụ để chạy trên một chuỗi nền. Tôi không muốn chờ đợi khi hoàn thành nhiệm vụ.

Trong .net 3.5, tôi sẽ làm điều này:

ThreadPool.QueueUserWorkItem(d => { DoSomething(); });

Trong .net 4, TPL là cách được đề xuất. Mô hình phổ biến mà tôi đã thấy được đề xuất là:

Task.Factory.StartNew(() => { DoSomething(); });

Tuy nhiên, StartNew()phương thức trả về một Taskđối tượng thực hiện IDisposable. Điều này dường như bị bỏ qua bởi những người giới thiệu mẫu này. Tài liệu MSDN về Task.Dispose()phương pháp cho biết:

"Luôn gọi Vứt bỏ trước khi bạn phát hành tham chiếu cuối cùng của mình cho Tác vụ."

Bạn không thể gọi hủy bỏ một nhiệm vụ cho đến khi nó hoàn thành, vì vậy, việc để luồng chính chờ và xử lý lệnh gọi sẽ đánh bại quan điểm của việc thực hiện trên một luồng nền ngay từ đầu. Có vẻ như không có bất kỳ sự kiện đã hoàn thành / kết thúc nào có thể được sử dụng để dọn dẹp.

Trang MSDN trên lớp Tác vụ không bình luận về điều này và cuốn sách "Pro C # 2010 ..." đề xuất mẫu tương tự và không đưa ra bình luận về việc xử lý tác vụ.

Tôi biết nếu tôi cứ để nó thì cuối cùng thì trình hoàn thiện sẽ bắt được nó, nhưng liệu điều này có quay lại và cắn tôi khi tôi đang thực hiện rất nhiều nhiệm vụ và quên như thế này và chuỗi trình hoàn thiện bị quá tải không?

Vì vậy, câu hỏi của tôi là:

  • Không gọi Dispose()vào Tasklớp trong trường hợp này có được chấp nhận không ? Và nếu có, tại sao và có rủi ro / hậu quả không?
  • Có tài liệu nào thảo luận về điều này không?
  • Hay có cách nào thích hợp để vứt bỏ Taskđối tượng mà tôi đã bỏ sót?
  • Hay có một cách khác để thực hiện nhiệm vụ cứu hỏa và quên với TPL?

Câu trả lời:


108

Có một cuộc thảo luận về điều này trong các diễn đàn MSDN .

Stephen Toub, một thành viên của nhóm pfx của Microsoft có điều này để nói:

Task.Dispose tồn tại do Tác vụ có khả năng bao bọc một xử lý sự kiện được sử dụng khi chờ tác vụ hoàn thành, trong trường hợp chuỗi chờ thực sự phải chặn (trái ngược với việc quay hoặc có khả năng thực thi tác vụ mà nó đang chờ). Nếu tất cả những gì bạn đang làm là sử dụng tính năng liên tục, thì xử lý sự kiện đó sẽ không bao giờ được phân bổ
...
tốt hơn là bạn nên dựa vào việc hoàn thiện để xử lý mọi thứ.

Cập nhật (tháng 10 năm 2012)
Stephen Toub đã đăng một blog có tiêu đề Tôi có cần phải hủy bỏ Công việc không? cung cấp một số chi tiết hơn và giải thích các cải tiến trong .Net 4.5.

Tóm lại: Bạn không cần phải vứt bỏ Taskđồ vật trong 99% thời gian.

Có hai lý do chính để xử lý một đối tượng: để giải phóng tài nguyên không được quản lý một cách kịp thời, xác định và để tránh chi phí chạy trình hoàn thiện của đối tượng. Cả hai điều này đều không áp dụng cho Taskhầu hết thời gian:

  1. Tính đến Net 4.5, lần duy nhất một Taskgiao đất tay cầm chờ đợi nội bộ (tài nguyên duy nhất không được quản lý trong các Taskđối tượng) là khi bạn sử dụng một cách rõ ràng IAsyncResult.AsyncWaitHandletrong những Task, và
  2. Bản Taskthân đối tượng không có trình hoàn thiện; tay cầm tự nó được bao bọc trong một đối tượng có trình hoàn thiện, vì vậy trừ khi nó được cấp phát, không có trình hoàn thiện nào để chạy.

3
Cảm ơn, thú vị. Nó đi ngược lại tài liệu MSDN. Có bất kỳ lời chính thức nào từ MS hoặc nhóm .net rằng đây là mã có thể chấp nhận được không. Ngoài ra còn có các điểm nêu ra ở phần cuối của cuộc thảo luận rằng "những gì nếu việc thực hiện thay đổi trong một phiên bản tương lai"
Simon P Stevens

Trên thực tế, tôi vừa nhận thấy rằng người trả lời trong chuỗi đó thực sự làm việc tại microsoft, dường như thuộc nhóm pfx, vì vậy tôi cho rằng đây là một câu trả lời chính thức. Nhưng có gợi ý về phía dưới của nó không hoạt động trong mọi trường hợp. Nếu có khả năng bị rò rỉ, tôi nên hoàn nguyên về ThreadPool.QueueUserWorkItem mà tôi biết là an toàn?
Simon P Stevens

Có, rất lạ là có một Dispose mà bạn có thể không gọi. Nếu bạn xem các mẫu tại đây msdn.microsoft.com/en-us/library/dd537610.aspx và tại đây msdn.microsoft.com/en-us/library/dd537609.aspx thì chúng không sắp xếp nhiệm vụ. Tuy nhiên, các mẫu mã trong MSDN đôi khi thể hiện các kỹ thuật rất tệ. Anh chàng cũng trả lời về câu hỏi làm việc cho Microsoft.
Kirill Muzykov

2
@Simon: (1) MSDN doc bạn trích dẫn là lời khuyên chung chung, trường hợp cụ thể có lời khuyên cụ thể hơn (vd: không cần dùng EndInvoketrong WinForms khi dùng BeginInvokeđể chạy code trên UI thread). (2) Stephen Toub là một diễn giả thường xuyên về việc sử dụng PFX một cách hiệu quả (ví dụ: trên channel9.msdn.com ), vì vậy nếu ai có thể hướng dẫn tốt thì anh ấy chính là người đó. Lưu ý đoạn thứ hai của anh ấy: có những lúc để mọi thứ cho người hoàn thiện thì tốt hơn.
Richard

12

Đây là loại vấn đề tương tự như với lớp Thread. Nó tiêu thụ 5 tay cầm hệ điều hành nhưng không triển khai IDisposable. Quyết định tốt của các nhà thiết kế ban đầu, tất nhiên có rất ít cách hợp lý để gọi phương thức Dispose (). Trước tiên, bạn phải gọi Join ().

Lớp Tác vụ thêm một tay cầm vào điều này, một sự kiện đặt lại thủ công nội bộ. Tài nguyên hệ điều hành rẻ nhất hiện có. Tất nhiên, phương thức Dispose () của nó chỉ có thể giải phóng một xử lý sự kiện đó, không phải 5 xử lý mà Thread sử dụng. Ừ, đừng bận tâm .

Hãy cẩn thận rằng bạn phải quan tâm đến thuộc tính IsFaulted của nhiệm vụ. Đó là một chủ đề khá xấu xí, bạn có thể đọc thêm về nó trong bài viết này của Thư viện MSDN . Một khi bạn giải quyết vấn đề này đúng cách, bạn cũng nên có một vị trí tốt trong mã của mình để xử lý các nhiệm vụ.


6
Nhưng một tác vụ không tạo ra Threadtrong hầu hết các trường hợp, nó sử dụng ThreadPool.
svick

-1

Tôi rất muốn thấy ai đó cân nhắc kỹ thuật được hiển thị trong bài đăng này: Lệnh gọi đại biểu không đồng bộ có thể cháy và quên an toàn trong C #

Có vẻ như một phương thức mở rộng đơn giản sẽ xử lý tất cả các trường hợp nhỏ khi tương tác với các tác vụ và có thể gọi vứt bỏ nó.

public static void FireAndForget<T>(this Action<T> act,T arg1)
{
    var tsk = Task.Factory.StartNew( ()=> act(arg1),
                                     TaskCreationOptions.LongRunning);
    tsk.ContinueWith(cnt => cnt.Dispose());
}

3
Tất nhiên điều đó không thể xử lý đối tượng được Tasktrả về ContinueWith, nhưng hãy xem trích dẫn từ Stephen Toub là câu trả lời được chấp nhận: không có gì để xử lý nếu không có gì thực hiện chờ chặn trên một nhiệm vụ.
Richard

1
Như Richard đã đề cập, ContinueWith (...) cũng trả về một đối tượng Tác vụ thứ hai mà sau đó không bị xử lý.
Simon P Stevens

1
Vì vậy, như là viết tắt của mã ContinueWith thực sự tồi tệ hơn so với redudant vì nó sẽ khiến một tác vụ khác được tạo ra chỉ để loại bỏ tác vụ cũ. Với cách làm này, về cơ bản sẽ không thể đưa lệnh chờ chặn vào khối mã này ngoài việc nếu ủy nhiệm hành động mà bạn đã chuyển vào nó đang cố gắng thao túng bản thân Tác vụ cũng đúng?
Chris Marisic

1
Bạn có thể sử dụng cách lambdas nắm bắt các biến theo một cách hơi phức tạp để thực hiện nhiệm vụ thứ hai. Task disper = null; disper = tsk.ContinueWith(cnt => { cnt.Dispose(); disper.Dispose(); });
Gideon Engelberth

@GideonEngelberth dường như sẽ phải hoạt động. Vì người xử lý sẽ không bao giờ bị GC xử lý nên nó sẽ vẫn có giá trị cho đến khi lambda gọi chính nó để loại bỏ, giả sử rằng tham chiếu vẫn còn hiệu lực ở đó / shrug. Có lẽ cần một lần thử / bắt trống xung quanh nó?
Chris Marisic
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.