1. 程式人生 > >為什麼要小心使用 Task.Run

為什麼要小心使用 Task.Run

昨天在部落格園有園友問了我一個問題,是這樣的: ![](https://w-share.oss-cn-shanghai.aliyuncs.com/20201202213205.png) 先是半個月前 @碧水青荷 童鞋的一句話“大家都說不要隨便 `Task.Run(()=>{})` 這樣寫”,當時沒有想太多,這句話並沒有引起我注意,只顧著回答他“不想在程式碼中加 `async/await` 該怎麼做”的問題。 然後這句話被 @褲兜 童鞋注意到,昨天問了我為什麼。我當時也很納悶,`Task.Run` 在**並行**場景中很常見啊,為什麼大家會有不要隨便使用的說法。很遺憾,我當時腦海裡認為這種說法只是空穴來風,並沒有細究。 我有個習慣,就是下班路上在地鐵上快速覆盤一下今天發生的事情。當時這個問題剛好就在腦海裡閃現了一下,“為什麼大家都說不要隨便使用 `Task.Run`”。突然想起了多年前的一個晚上……哦,難道是“Ta”? 對,應該就是它,**記憶體洩露**,除了這個原因我再也想不到其它原因了。因為我隱約記得多年前我確實踩過一次這個坑,也可能是兩次。 沒錯,`Task.Run` 使用不當,一不留意就會有記憶體洩露的問題。 我們先來看一段程式碼: ```csharp public class MyClass { private int _id; private Logger _logger; public MyClass(Logger logger) { _logger = logger; } public Task Foo(Logger logger) { return Task.Run(() => { _logger.LogInformation($"Executing job with ID {_id}"); // do sth. }); } } ``` 在這段程式碼中,私有成員 `_id` 被 `Task.Run` 的匿名方法捕獲使用,進而導致 `MyClass` 例項被引用。當外部使用完 `MyClass` 例項時,本該由 GC 回收的時候卻發現它還被其它資源引用著,所以 GC 認為該例項不應用被回收,也就可能永遠失去了被回收的機會。 道理很簡單,我就不再用示例演示了。解決辦法也很簡單,想必很多人都知道,就是使用**本地變數**。 ```csharp public class MyClass { private int _id; private Logger _logger; public MyClass(Logger logger) { _logger = logger; } public Task Foo(Logger logger) { var localId = _id; return Task.Run(() => { _logger.LogInformation($"Executing job with ID {localId}"); // do sth. }); } } ``` 通過將值分配給一個本地變數,類就沒有成員被捕獲,即避免了潛在的記憶體洩漏。 記憶體洩漏問題在 `Task.Run` 身上發生很常見,容易被大家記住,容易提高警覺。其實不光是 `Task.Run`,其它地方使用了匿名方法也同樣要小心,比如這個示例: ```csharp public class MyClass { private int _id; private Logger _logger; private JobQueue _jobQueue; public MyClass(Logger logger, JobQueue jobQueue) { _logger = logger; _jobQueue = jobQueue; } public void Foo() { _jobQueue.EnqueueJob(() => { _logger.LogInformation($"Executing job with ID {_id}"); // do sth. }); } } ``` 也有記憶體洩漏的問題。 總之,**任何使用匿名方法的地方都要避免捕獲類的成員,小心記憶體洩漏**。