1.簡介 C#中通常使用線程類Thread來進行線程的創建與調度,博主在本文中將分享多年C#開發中遇到的Thread使用陷阱。 Thread調度其實官方文檔已經說明很詳細了。本文只簡單說明,不做深入探討。 如下代碼展示了一個線程的創建與啟動 static void Main(string[] args) { Thread thd = new Thread(new ThreadStart(TestThread)); thd.IsBackground = false; thd.Start(); } static void TestThread() { while (true) { Thread.Sleep(1); } } 我們可以通過 Thread.ThreadState 判斷指定線程狀態 Thread.Yield 切換線程 Thread .Interrupt 引發阻塞線程的中斷異常 Thread .Join 等待線程完成 Thread.Abort 引發線程上的ThreadAborting異常 2.Abort陷阱的產生 本文要談的是Thread.Abort。有一定多線程開發經驗的朋友一定聽說過它。官方文檔如此描述: 1 在調用此方法的線程上引發 ThreadAbortException,以開始終止此線程的過程。 調用此方法通常會終止線程。 這在實際中是非常有用的,相信大部分人都會迫不及待地在項目中用上Thread.Abort來終止線程(博主就是迫不及待地用到項目中了)。不過對於不熟悉的API,使用之前一定先看懂文檔(這是博主在吃過不少虧后的感言) Abort調用還分為線程自身調用: 當線程對自身調用 Abort 時,效果類似於引發異常; ThreadAbortException 會立刻發生,並且結果是可預知的。 但是,如果一個線程對另一個線程調用 Abort,則將中斷運行的任何代碼。 還有一種可能就是靜態構造函數被終止。在極少數情況下,這可以防止在該應用程序域中創建該類的實例。在 .NET Framework 1.0 版和 1.1 版中,在 finally 塊運行時線程可能會中止,在這種情況下, finally 塊將被中止。 被其它線程調用: 如果正在中止的線程是在受保護的代碼區域,如 catch 塊、 finally 塊或受約束的執行區域,可能會阻止調用 Abort 的線程。 如果調用 Abort 的線程持有中止的線程所需的鎖定,則會發生死鎖。 由官方文檔上的說明可知:Abort方法調用是需要特別注意避免靜態構造函數的終止和鎖的使用,這是通過文檔我們能夠獲得的信息。 但不是全部! 陷阱一:線程中代碼ThreadAbortException異常的處理 舉個栗子 class Program { static TcpClient m_TcpClient = new TcpClient(); static void Main(string[] args) { m_TcpClient.Connect("192.168.31.100" , 8888); Thread thd = new Thread(new ThreadStart(TestThread)); thd.IsBackground = false; thd.Start(); Console.ReadKey(); Console.Write("線程Abort!"); thd.Abort(); Console.ReadKey(); } static void TestThread() { while (true) { byte[] sdDat = new byte[10000 * 1024]; try { Thread.Sleep(1); m_TcpClient.GetStream().Write(sdDat, 0, sdDat.Length); } catch (Exception ex) { // 異常處理 m_TcpClient.Close(); } } } } 以上代碼創建了一個Tcp連接,然后不間斷向服務端發送數據流。此時若服務端某種原因使用了Thread.Abort終止發送數據(只是終止發送數據,並不是要斷開連接),那執行的結果與期望便大相徑庭了。 這里正確的使用方式是在發生SocketException異常和ThreadAbortException 異常時分別處理 catch (SocketException sckEx) { //socket異常處理 m_TcpClient.Close(); }catch(ThreadAbortException thAbortEx) { } 在項目中大家都會遇到對第三方IO庫的調用,如果恰好第三方庫缺少對ThreadAbortException的異常處理,那你的代碼使用ThreadAbort出現BUG的概率便大大提高了。(實際上不光是第三方庫,.NetFramework中API也並非完全考慮了此異常)陷阱二就說明了一個系統API對此異常的處理缺陷。 - 陷阱二:文件操作 同樣我使用測試代碼說明文件操作API的一個異常情況。 開啟一個線程,對某個文件寫數據(不斷循環)代碼如下: class Program { static void Main(string[] args) { Thread thd = new Thread(new ThreadStart(TestThread)); thd.IsBackground = false; thd.Start(); Thread.Sleep(1000); //等待,確保代碼已經開始執行 while (true) { if (thd.IsAlive) { thd.Abort(); Thread.Sleep(10); } else { Console.WriteLine("線程已經退出!"); break; } } Console.ReadKey(); } static void TestThread() { while (true) { byte[] sdDat = new byte[10240]; try { using (FileStream fs = File.Open("D:\\1.dat", FileMode.OpenOrCreate, FileAccess.ReadWrite)) { fs.Write(sdDat, 0 , 10240); Thread.Sleep(1); // 根據自己的運行環境調節休眠時間 } } catch (IOException ex) { Console.WriteLine("IO exception:" + ex.Message); break; } catch (ThreadAbortException ) { Thread.ResetAbort(); Console.WriteLine("ThreadAbortException "); }catch(Exception ex) { Console.WriteLine("Other exception:" + ex.Message); break; } } Console.WriteLine("線程退出"); } } 運行代碼得到輸出: (實際並非每次輸出都一致,取決與執行代碼計算機的當前狀態,你也可以改變while循環中的休眠時間,輸出較多或較少行ThreadAbortException) ThreadAbortException ThreadAbortException ThreadAbortException IO exception:文件“D:\1.dat”正由另一進程使用,因此該進程無法訪問此文件。 線程退出 線程已經退出! 這次代碼里我使用ResetAbort處理ThreadAbortException異常,將Abort狀態恢復並繼續執行循環,而在IO異常與其它異常時候直接退出循環。 我們可以看到在 ThreadAbortException打印了三次后,出發了IO異常: IO exception:文件“D:\1.dat”正由另一進程使用,因此該進程無法訪問此文件。 這個異常是如何產生的呢?各位不妨看看代碼,分析下可能的原因。 首先,這段代碼“看起來”的確是沒有問題,大家知道在using里的new 的對象在代碼段結束的時候,會自動調用Dispose方法釋放資源。重最開始的兩次ThreadAbort異常被觸發可以看出,即使在這種情況下,被占用的文件資源也已經被釋放掉了。當然, “看起來”與實際的效果還是有差距,在第四次執行就觸發IOException了。說明第三次的文件被占用后沒有釋放。 問題的關鍵就在第三次占用文件后為什么沒有被釋放? 我猜測有可能是fs的對象引用在賦值到fs之前就觸發了ThreadAbortException異常,而File.Open代碼中占用了文件資源后並在返回之前沒有處理ThreadAbortException,導致在using代碼段結束釋放時,fs為空引用,那自然就無法調用其釋放的代碼了。當然這些只是我的大膽猜測,為此我修改了本例中TestThread方法的代碼,驗證猜測。 static void TestThread() { byte[] sdDat = new byte[10240]; FileStream fs = null; while (true) { try { fs = null; using (fs = File.Open("D:\\1.dat", FileMode.OpenOrCreate, FileAccess.ReadWrite)) { fs.Write(sdDat, 0, 10240); Thread.Sleep(1); } } catch (IOException ex) { Console.WriteLine("IO exception:" + ex.Message); break; } catch (ThreadAbortException ex) { Thread.ResetAbort(); Console.WriteLine("ThreadAbortException (fs == null):[{0}]", fs == null); } catch (Exception ex) { Console.WriteLine("Other exception:" + ex.Message); break; } } Console.WriteLine("線程退出"); } 我把fs變量提出到while循環之前,並在 每次調用using 代碼段之前賦值為null,隨后每次觸發ThreadAbortException 時都打印出fs是否為空。 執行結果: ThreadAbortException (fs == null):[False] ThreadAbortException (fs == null):[True] ThreadAbortException (fs == null):[False] ThreadAbortException (fs == null):[False] ThreadAbortException (fs == null):[False] ThreadAbortException (fs == null):[False] ThreadAbortException (fs == null):[False] ThreadAbortException (fs == null):[False] ThreadAbortException (fs == null):[True] IO exception:文件“D:\1.dat”正由另一進程使用,因此該進程無法訪問此文件。 線程退出 線程已經退出! 多次執行程序就可以看出,每次IOException異常發送的時候,上次打印的 fs都為空。那怎么解釋中間有一次fs為空但沒有觸發IOException呢?記得我上面的分析嗎?File.Open在占用了文件資源后並在返回之前的Exception沒有處理會出現問題,那么在占用文件資源之前出現Exception是不會出現占用資源未釋放的問題的。所以,問題的原因正如我分析的那樣。一個需要釋放的類(資源)是不太適宜在可能會被Abort的線程中創建並釋放的。因為你不太可能完全保證資源占用的時候類賦值之前不會觸發ThreadAbortException的。 3.結尾 在項目開發中,Thread類就如同一把雙刃劍,功能強大得不得了,但是給代碼理解與調試帶來了一定程度上的困難。如果非要問我在多線程開發上有什么建議的話,我想說,除非你已經在千錘百煉的開發經驗中完全掌握了多線程,否則能不用它就不要用它吧!