如何在没有主线程等待的情况下从长时间运行的后台任务中获取异常

时间:2012-12-26 08:50:41

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

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

我决定使用BackgroundWorker。但是.NET 4.0提供Task Task Parallel Library类,根据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()调用使当前线程等待后台任务完成!

我可以使用TPL的{​​{1}}这样的场景,其中主线程不应该等到后台任务完成,但同时,每当引发异常时它都会得到异常细节从后台任务?

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

2 个答案:

答案 0 :(得分:3)

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

  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:表单     {

        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();
    
        }
    }
    

答案 1 :(得分:1)

从表单本身开始长时间运行操作,而不是在创建表单之前。请记住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).
        }
    }
}