C#中的線程之Abort陷阱


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類就如同一把雙刃劍,功能強大得不得了,但是給代碼理解與調試帶來了一定程度上的困難。如果非要問我在多線程開發上有什么建議的話,我想說,除非你已經在千錘百煉的開發經驗中完全掌握了多線程,否則能不用它就不要用它吧!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM