在不同的线程中抛出异常时如何捕获

130

我的一个方法 (Method1) 开启了一个新线程。 该线程执行一个方法 (Method2),在执行过程中抛出异常。 我需要在调用方法 (Method1) 中获取该异常信息。

有没有办法在 Method2 中抛出的异常被 Method1 捕获呢?

5个回答

212

.NET 4及以上版本中,您可以使用Task<T>类来替换创建新线程。然后,您可以使用任务对象上的.Exceptions属性获取异常。

有两种方法可以实现:

  1. 在单独的方法中:
    // 您在某个任务的线程中处理异常

class Program
{
    static void Main(string[] args)
    {
        Task<int> task = new Task<int>(Test);
        task.ContinueWith(ExceptionHandler, TaskContinuationOptions.OnlyOnFaulted);
        task.Start();
        Console.ReadLine();
    }

    static int Test()
    {
        throw new Exception();
    }

    static void ExceptionHandler(Task<int> task)
    {
        var exception = task.Exception;
        Console.WriteLine(exception);
    }
}
  • 在同一个方法中:// 您在调用者线程中处理异常

  • class Program
    {
        static void Main(string[] args)
        {
            Task<int> task = new Task<int>(Test);
            task.Start();
    
            try
            {
                task.Wait();
            }
            catch (AggregateException ex)
            {
                Console.WriteLine(ex);    
            }
    
            Console.ReadLine();
        }
    
        static int Test()
        {
            throw new Exception();
        }
    }
    
    请注意,您获得的异常是AggregateException。所有真正的异常都可以通过ex.InnerExceptions属性获得。
    在.NET 3.5中,您可以使用以下代码:
  • //您可以在子线程中处理异常

  • class Program
    {
        static void Main(string[] args)
        {
            Exception exception = null;
            Thread thread = new Thread(() => SafeExecute(() => Test(0, 0), Handler));
            thread.Start();            
    
            Console.ReadLine();
        }
    
        private static void Handler(Exception exception)
        {        
            Console.WriteLine(exception);
        }
    
        private static void SafeExecute(Action test, Action<Exception> handler)
        {
            try
            {
                test.Invoke();
            }
            catch (Exception ex)
            {
                Handler(ex);
            }
        }
    
        static void Test(int a, int b)
        {
            throw new Exception();
        }
    }
    
  • 或者 // 在调用方线程中处理异常

  • class Program
    {
        static void Main(string[] args)
        {
            Exception exception = null;
            Thread thread = new Thread(() => SafeExecute(() => Test(0, 0), out exception));
    
            thread.Start();            
    
            thread.Join();
    
            Console.WriteLine(exception);    
    
            Console.ReadLine();
        }
    
        private static void SafeExecute(Action test, out Exception exception)
        {
            exception = null;
    
            try
            {
                test.Invoke();
            }
            catch (Exception ex)
            {
                exception = ex;
            }
        }
    
        static void Test(int a, int b)
        {
            throw new Exception();
        }
    }
    

    抱歉,我忘了提到我正在使用.NET 3.5。据我所知,Task是4.0的东西? - Silverlight Student
    3
    好的,我刚刚更新了我的回答以满足您的要求。 - oxilumin
    @oxilumin:谢谢你,感激不尽。还有一个后续问题。如果您的Test()方法也需要一些参数,那么您将如何修改SafeExecute方法来处理这些参数? - Silverlight Student
    2
    @SilverlightStudent 在这种情况下,我将传递一个 lambda 表达式,而不是 Test。就像 () => Test(myParameter1, myParameter2) 这样。 - oxilumin
    @oxilumin:再次感谢您,但是当我尝试执行时,出现了编译错误。您能否更新上述示例以适应必须传递多个参数的情况? - Silverlight Student
    显示剩余2条评论

    11

    Method1 中无法捕获异常。但是在 Method2 中可以捕获异常并记录到一个变量中,原始执行线程可以读取并处理该变量。


    感谢您的回复。所以如果Method1是Class1的一部分,并且我在该类中有一个类型为Exception的变量。每当Method2抛出异常时,它也会在Class1中设置该异常变量。这听起来像一个合理的设计吗?有没有处理这种情况的最佳实践方法? - Silverlight Student
    没错,你只需要存储异常并稍后访问它。将来运行的方法(特别是当Method2完成时的回调)重新抛出该异常,就好像它们自己引起了异常一样,并不罕见,但这取决于你想要什么。 - ermau

    1
    我有一个特定的问题,我想使用包含控件的项目来进行集成测试,所以必须创建STA线程。以下是我最终得到的代码,放在这里以防其他人遇到相同的问题。
        public Boolean? Dance(String name) {
    
            // Already on an STA thread, so just go for it
            if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) return DanceSTA(name);
    
            // Local variable to hold the caught exception until the caller can rethrow
            Exception lException = null;
    
            Boolean? lResult = null;
    
            // A gate to hold the calling thread until the called thread is done
            var lGate = new ManualResetEvent(false);
    
            var lThreadStart = new ThreadStart(() => {
                try {
                    lResult = DanceSTA(name);
                } catch (Exception ex) {
                    lException = ex;
                }
                lGate.Set();
            });
    
            var lThread = new Thread(lThreadStart);
            lThread.SetApartmentState(ApartmentState.STA);
            lThread.Start();
    
            lGate.WaitOne();
    
            if (lException != null) throw lException;
    
            return lResult;
        }
    
        public Boolean? DanceSTA(String name) { ... }
    

    这是原样粘贴的代码。对于其他用途,我建议将操作或函数作为参数提供,并在线程上调用它,而不是硬编码调用方法。

    0
    最简单的在不同线程之间共享数据的方法是使用共享数据,示例如下(部分伪代码):
    class MyThread
    {
       public string SharedData;
    
       public void Worker()
       {
          ...lengthy action, infinite loop, etc...
          SharedData = "whatever";
          ...lengthy action...
          return;
       }
    }
    
    class Program
    {
       static void Main()
       {
          MyThread m = new MyThread();
          Thread WorkerThread = new Thread(m.Worker);
          WorkerThread.Start();
    
          loop//or e.g. a Timer thread
          {
             f(m.SharedData);
          }
          return;
       }
    }
    

    你可以在这篇关于多线程的好文章中了解到这种方法,但是我更喜欢在阅读《C# 3.0 in a nutshell》这本由Albahari兄弟(2007)编写的O'Reilly书籍中了解它。这本书也可以在Google Books上免费获取,因为它还涵盖了线程池、前台与后台线程等内容,并提供了简单易懂的示例代码。(免责声明:我拥有一本破旧的这本书)

    如果你正在制作一个WinForms应用程序,使用共享数据尤其方便,因为WinForm控件不是线程安全的。使用回调将数据从工作线程传递回主UI线程的WinForm控件需要使用Invoke()编写丑陋的代码来使该控件线程安全。相反,使用共享数据和单线程的System.Windows.Forms.Timer,并设置短的Interval,例如0.2秒,你可以轻松地将信息从工作线程发送到控件而无需使用Invoke


    -1

    这是我用来将异常抛回到主线程以便捕获的代码。

    class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cancelToken = new CancellationTokenSource();
            Exception taskException = null;
    
            var timerTask = Task.Factory.StartNew(() =>
            {
                for (;;)
                {
                    if (cancelToken.IsCancellationRequested)
                        break;
    
                    ContinuousTask();
    
                    Thread.Sleep(400);
    
                }
            }, cancelToken.Token).ContinueWith((t, o) => {
                taskException = t.Exception;
                ((Thread)o).Interrupt();
            }, Thread.CurrentThread, TaskContinuationOptions.OnlyOnFaulted);
    
            try
            {
                
                //do a bunch of tasks here
    
                //want to skip the do while and go to the catch if exception is thrown
                do
                {
                    System.Threading.Thread.Sleep(200);
                } while (true);
    
            }
            catch
            {
                if (taskException != null)
                    Console.WriteLine(taskException.Message);
            }
            
        }
    
        private static int _loopCounter = 0;
        public static void ContinuousTask()
        {
            int counter = 0;
    
            do
            {
    
                if (_loopCounter >= 3)
                    throw new Exception("error");
    
                if (counter >= 5)
                    break;
    
                counter += 1;
                System.Threading.Thread.Sleep(100);
    
            } while (true);
    
            _loopCounter += 1;
        }
    
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接