【问题标题】:How to get an exception raised from a long running background Task without having main thread to wait如何在没有主线程等待的情况下从长时间运行的后台任务中引发异常
【发布时间】:2012-12-11 20:48:27
【问题描述】:

我想在后台线程上执行一些长时间运行的操作(例如监听操作系统引发的一些事件)。大多数情况下,操作会连续运行而不会出现任何问题。但在某些罕见的情况下,操作系统级别的 API 会发送一些错误代码,我需要从后台线程引发异常,该异常必须传播到主线程以将其显示给我的 WinFrom 应用程序的用户。

我决定为此使用BackgroundWorker。但是 .NET 4.0 提供了 Task Parallel LibraryTask 类,根据 TPL 上的各种博客,这是一个更好的选择。

在我的应用程序中,我必须在显示实际表单之前启动后台任务。由于实际代码相当复杂,我写了一些模拟实时问题的示例代码:

public static Task task;

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            ThreadTest tt = new ThreadTest();
            task = new Task(() => tt.PerformTask("hi"));
            task.Start();
            try
            {
                task.Wait();
            }
            catch (AggregateException aggregateException)
            {
                // Handle exception here.
            }

            Application.Run(new Form1());
        }

在这段代码中,我从来没有看到主窗体仅仅是因为后台任务一直在无异常运行,而task.Wait() 调用使当前线程等待后台任务完成!

我可以使用TPLTask 来处理主线程不应等到后台任务完成但同时在后台任务引发异常时获取异常详细信息的情况?

在上面的代码中,解决方案之一可能是在稍后阶段移动任务创建代码。但在这种情况下,我的问题更具学术性。

【问题讨论】:

    标签: multithreading c#-4.0 exception-handling task-parallel-library


    【解决方案1】:

    是的,你可以。请看下面的代码。

    1. 程序代码为:

           /// <summary>
      /// The main entry point for the application.
      /// </summary>
      [STAThread]
      static void Main()
      {
          Application.EnableVisualStyles();
          Application.SetCompatibleTextRenderingDefault(false);
      
          CancellationTokenSource  cancellationTokenSource = new CancellationTokenSource();
      
          Task  longRunningTask = new Task((state) =>
              {
                  LongRunningWork.DoWork(  cancellationTokenSource.Token);
      
              },cancellationTokenSource.Token,TaskCreationOptions.LongRunning);
      
          var newForm = new Form1(cancellationTokenSource); 
          new Thread((state) =>
              {
                  longRunningTask.Start();
      
                  try
                  {
                      longRunningTask.Wait();
                  }
                  catch (AggregateException exception)
                  {
                      Action<Exception> showError = (ex) => MessageBox.Show(state as Form, ex.Message);
      
                      var mainForm = state as Form;
                      if (mainForm != null)
                      {
                          mainForm.BeginInvoke(showError, exception.InnerException);
                      }
      
                  }
              }).Start(newForm);
          Application.Run(newForm);
      
    2. 长时间运行任务的代码是:

      public class LongRunningWork
      {
          public static void DoWork( CancellationToken cancellationToken)
          {
      
              int iterationCount = 0;
              //While the 
              while (!cancellationToken.IsCancellationRequested &&iterationCount <5)
              {
                  //Mimic that we do some long jobs here
                  Thread.Sleep(1000);
      
                  iterationCount++;
                  //The jobs may throw the exception on the specific condition
                  if (iterationCount ==5)
                  {
                      throw  new InvalidOperationException("Invalid action");
                  }
      
      
              }
      
              //cancel the task 
              cancellationToken.ThrowIfCancellationRequested();
          }
      }
      
    3. 最后是Form1的代码,其中包括一个退出按钮,其功能是在点击时终止程序。

      公共部分类 Form1 : Form {

          private CancellationTokenSource _cancellationTokenSource;
      
          public Form1()
          {
              InitializeComponent();
          }
      
          public Form1(CancellationTokenSource cancellationTokenSource):this()
          {
              _cancellationTokenSource = cancellationTokenSource;
          }
      
          private void exitBtn_Click(object sender, EventArgs e)
          {
              //Cancel out the task
              if (_cancellationTokenSource != null)
              {
                  _cancellationTokenSource.Cancel();
              }
      
              //Exit the program
              Application.Exit();
      
          }
      }
      

    【讨论】:

    • 这真的很有帮助。谢谢你。但是,在您的代码中,异常也不会传播到“主”线程。它被传播到启动任务的线程。如果我们需要显示模态 MessageBox 的主窗体必须作为模态 MessageBox 的所有者传递,这将产生问题。不是吗?
    • 太棒了!为我工作。 :)
    【解决方案2】:

    从表单本身开始您的长期运行操作,而不是在创建表单之前。请记住,Application.Run() 在当前线程上启动了一个消息循环,但这意味着您可以使用该消息循环从 Timer 类中轮询您的任务。

    class Form1 : Form
    {
        private Timer PollingTimer;
        private Task BackgroundTask;
    
        public Form1()
        {
            InitializeComponent();
    
            // Begin the background task.
            ThreadTest tt = new ThreadTest();
            this.BackgroundTask = new Task(() => tt.PerformTask("hi"));
            this.BackgroundTask.Start();
    
            // Monitor the task's status by polling it regularly.
            this.PollingTimer = new Timer();
            this.PollingTimer.Interval = 1000;        // In milliseconds.
            this.PollingTimer.Tick += timerCallback;
            this.PollingTimer.Start();
        }        
    
        private timerCallback(object sender, EventArgs e)
        {
            if (this.BackgroundTask.IsFaulted)
            {
                // Exception information is in BackgroundTask.Exception.
            }
        }
    }
    

    如果您不喜欢轮询(我喜欢),您需要从任务中捕获异常并将其编组回您的 UI 线程。最好的方法就是不捕获任务本身的异常,并提供一个仅在错误时执行的延续方法。

    class Form1 : Form
    {
        private Task BackgroundTask;
    
        public Form1()
        {
            InitializeComponent();
    
            // Capture the UI thread context.
            // (Note, it may be safer to run this in the Form.Load event than the constructor.
            var uiContext = TaskScheduler.FromCurrentSynchronizationContext();
    
            // Begin the background task.
            ThreadTest tt = new ThreadTest();
            this.BackgroundTask = new Task(() => tt.PerformTask("hi"))
                // Schedule a continuation to be executed after the task is completed.
                .ContinueWith((t,arg) => 
                {
                    // Exception information is in t.Exception
                },null, null, 
                // Only execute the continuation if the task throws an exception.
                TaskContinuationOptions.OnlyOnFaulted,
                // Execute the continuation on the UI thread we captured above. 
                uiContext);
            this.BackgroundTask.Start();
        }        
    }
    

    Task.ContinueWith()TaskScheduler.FromCurrentSynchronizationContext() 的 MSDN 参考资料。

    而且,如果您拥有带有 asyncawait 的 .NET 4.5:

    class Form1 : Form
    {
        private Task BackgroundTask;
    
        public Form1()
        {
            InitializeComponent();
        }        
    
        private async void Form1_Load(object sender, EventArgs e)
        {
            ThreadTest tt = new ThreadTest();
            try
            {
                // Move your Task creation and start logic into a method.
                await tt.RunAsync();
            } 
            catch (Exception ex)
            {
                // Really smart compiler writers make sure you're on the right thread 
                // and everything Just Works(tm).
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2015-11-02
      • 1970-01-01
      • 1970-01-01
      • 2019-10-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多