zoukankan      html  css  js  c++  java
  • 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类就如同一把双刃剑,功能强大得不得了,但是给代码理解与调试带来了一定程度上的困难。如果非要问我在多线程开发上有什么建议的话,我想说,除非你已经在千锤百炼的开发经验中完全掌握了多线程,否则能不用它就不要用它吧!
  • 相关阅读:
    466 你真的了解回流和重绘吗
    465 从一道题浅说 JavaScript 的事件循环
    460 动态组件
    458 MongoDB安装
    457 vue d13
    450 for of 循环
    六种排序算法的Js实现
    你真的理解了比较运算符吗?
    5个有趣且不必要的 JavaScipt 技巧
    7个常见的 JavaScript 测验及解答
  • 原文地址:https://www.cnblogs.com/smartsmile/p/12067322.html
Copyright © 2011-2022 走看看