摘要:本博文解釋在.NET 4.X中的Task使用完后為什么不應該調用Dispose()。並且說明.NET4.5對.NET4.0的Task對象進行的部分改進:減輕Task對WaitHandle對象的依賴,並且增強在釋放了Task后對其成員的可訪問性。
我多次獲得這樣一個問題:
“Task實現了IDisposable接口並且公開Dispose()方法,這是否意味着我們要對所有的任務進行釋放嗎?”
概述
1. 這是我對該問題的簡要回答:
“不是,不用釋放你持有的Task。”
2. 這是我對該問題的中篇回答:
“不是,不用釋放你持有的Task,除非性能報告或可伸縮性測試報告顯示你需要釋放Task以滿足你的性能目標。如果你發現一個需要被釋放的Task,必須100%確保在你的釋放點處該Task已經完成並且沒有被其他地方在使用。”
3. 下面,你可以找一個休閑的閱讀時間,這是我對該問題的長回答:
為什么要調用Task的Dispose()?
.NET Framework設計指南中指出:一個類型如果持有其它實現過IDisposable接口的資源時,其自身也應該實現IDisposable接口。在Task內部,可能會分配一個WaitHandle對象用於等待任務完成。WaitHandle實現IDisposable接口因為它持有SafeWaitHandle內核等待句柄,所以Task實現了IDisposable接口。如果不主動釋放SafeWaitHandle句柄,最終終結器也會將其清理,但是不能對此資源立即清理並且還將此清理工作負荷遺留給系統。通過給Task實現IDisposable接口,我們可以讓開發人員能主動及時的對資源進行釋放。
問題
如果為每一個Task都分配一個WaitHandle,那么釋放Task將是一個好措施因為這樣能提高性能。但是事實並非如此,現實中,為Task分配WaitHandle的情況是非常少出現的。在.NET 4.0中,WaitHandle在以下幾種情況會延遲初始化:訪問 ((IAsyncResult)task).AsyncWaitHandle成員,或者調用Task的WaitAll()/WaitAny()方法(這兩個方法在.NET4.0版本中,內部是基於Task的WaitHandle對象實現的)。這使得回答“是否應該釋放Task”問題更加困難了,因為如果Task都使用了WaitAll()/WaitAny(),那么釋放Task就是一個好選擇。
public interface IAsyncResult { object AsyncState { get; } WaitHandle AsyncWaitHandle { get; } bool CompletedSynchronously { get; } bool IsCompleted { get; } }
在.NET 4.0中,一個Task一旦被釋放,它的大多數成員訪問都會拋出ObjectDisposedExceptions異常。這使得完成的任務很難被安全的緩存,因為一個消費者釋放Task后,另一個消費者無法再訪問Task的一些重要成員,如ContinueWith()方法或Result屬性。
這里還有另外一個問題:Task是基礎同步基元。如果Task被用於並行化,如在一個fork/join模式(”分支/合並”模式)中那么它就很容易知道什么時候完成它們和什么時候沒有人再使用它們,比如:
var tasks = new Task[3]; tasks[0] = Compute1Async(); tasks[1] = Compute2Async(); tasks[2] = Compute3Async(); Task.WaitAll(tasks); foreach(var task in tasks) task.Dispose();
然而,當使用Task的延續任務時,就很難判斷它什么時候完成它們和什么時候沒有人再使用它們,比如:
Compute1Async().ContinueWith(t1 => { t1.Dispose(); … });
示例成功的釋放掉Compute1Async()返回的Task,但是它忽略了如何釋放ContinueWith()返回的Task。當然,我們能使用同樣的方法釋放這個Task。
Compute1Async().ContinueWith(t1 => { t1.Dispose(); … }).ContinueWith(t2 => t2.Dispose());
但是我們不能釋放第二個ContinueWith()返回的Task。即使使用C#5.0中新的async/await異步方法也不能解決。例如:
string s1 = await Compute1Async(); string s2 = await Compute2Async(s1); string s3 = await Compute3Async(s2);
如果想釋放這些Task,我需要進行像下面這樣的重寫:
string s1 = null, s2 = null, s3 = null; using(var t1 = Compute1Async()) s1 = await t1; using(var t2 = Compute2Async(s1)) s2 = await t2; using(var t3 = Compute3Async(s2)) s3 = await t3;
解決方案
由於像上面這樣進行釋放大多數Task顯得很繁瑣,所以在.NET4.5中已經對Task的Dispose()做過一些改進:
1. 我們使得你更少機會為Task創建WaitHandle對象。在.NET4.5中我們已經重寫了Task的WaitAll()和WaitAny()以致這兩個方法不再依賴與WaitHandle對象(這樣WaitAll()、WaitAny()、Wait()就都基於自旋等待),避免在Task的內部實現中使用WaitHandle對象,並且提供async/await相關異步功能。因此,只有當你顯示訪問Task的IAsyncResult.AsyncWaitHandle成員才會為Task分配WaitHandle對象,但這種需求非常少見。這意味着除了這種非常少見的情況外,釋放一個任務是不需要的。
2. 我們使得Task在釋放后依然可用。你能使用Task的所有公開成員即使Task已經被釋放,訪問這些成員的表現就和釋放Task之前一樣。只有IAsyncResult.AsyncWaitHandle成員你不能使用,因為這是你釋放Task時真真所釋放的對象,當你嘗試在釋放Task后訪問這個屬性時依然會拋出ObjectDisposedException。此外,更進一步的說,現在我們推薦使用async/await異步方法以及基於任務的異步編程模式,降低對IAsyncResult的使用,即使你繼續使用((IAsyncResult)task),調用其AsyncWaitHandle成員也是十分罕見的。
3. Task.Dispose()方法在“.NET Metro風格應用程序”框架所引用的程序集中甚至並不存在(即此框架中Task沒有實現IDisposable接口)。
指南
所以,這又讓我們回到了簡要的回答:“不是,不用釋放你的Task。”通常很難找到一個合適的釋放點,目前幾乎沒有一個理由需要去主動釋放Task(因為調用((IAsyncResult)task).AsyncWaitHandle成員的需求是十分罕見的),並且在“.NET Metro風格應用程序”框架所引用的程序集中你甚至不能調用Task的Dispose()方法。
更多資源來源博文:關於Async與Await的FAQ
原文:http://blogs.msdn.com/b/pfxteam/archive/2012/03/25/10287435.aspx