从异步事件处理程序中捕获异常

时间:2017-05-01 15:28:14

标签: c# async-await

我有一个“后台作业”类,它以异步方式运行作业,并在进行任何进度时引发事件。如果此事件的事件处理程序引发异常,则调用方法永远不会捕获它。

如果我将事件处理程序切换为非异步。问题消失了。但是,我必须将所有从事件处理程序调用的异步调用转换为阻塞调用,而我宁愿不必这样做!

有没有办法捕获异步事件处理程序引发的异常?

using System;
using System.Threading.Tasks;

namespace AsyncEventHandlingIssue
{
    public class BackgroundJob
    {
        public int Progress { get; private set; }
        public event EventHandler ProgressUpdated;
        public async Task Start()
        {
            for (var i = 0; i < 100; i++)
            {
                await Task.Delay(1000);
                Progress++;
                ProgressUpdated?.Invoke(this, EventArgs.Empty);
            }
        }
    }

    public class Program
    {
        static async Task MainAsync()
        {
            var job = new BackgroundJob();
            job.ProgressUpdated += Job_ProgressUpdated;
            try
            {
                await job.Start();
            }
            catch(Exception ex)
            {
                Console.WriteLine($"The job failed with an error: {ex}");
            }
        }

        private static async void Job_ProgressUpdated(object sender, EventArgs e)
        {
            var job = (BackgroundJob)sender;
            await Task.Delay(100); // just an example - my real code needs to call async methods.
            Console.WriteLine($"The Job is at {job.Progress}%.");
            if (job.Progress == 5)
                throw new Exception("Something went wrong!");
        }

        static void Main(string[] args)
        {
            MainAsync().GetAwaiter().GetResult();
            Console.WriteLine("Reached the end of the program.");
            Console.ReadKey();
        }
    }
}

2 个答案:

答案 0 :(得分:2)

async void方法会在其捕获的上下文中引发它的try catch应该在async方法中的异常。此外,你的行为听起来很像旧的BackgroundWorker,不会重新发明轮子,并且已经有更好的选择来运行报告进度的async个工作。查看async进度更新here。这是一个利用async awaitasync进度更新的简单示例:

public class BackgroundJob {

    public async Task Start(IProgress<int> progress) {            
        for (var i = 0; i < 100; i++) {
            await Task.Delay(1000);
            progress.Report(i);
            //the method executing the job should determine something is wrong
            if (i == 5)
                throw new Exception("Something went wrong!");
        }
    }
}

public class Program {
    static async Task MainAsync() {
        var job = new BackgroundJob();
        var progress = new Progress<int>(Job_ProgressUpdated);
        try {
            await job.Start(progress);
        } catch (Exception ex) {
            //now your exception is caught
            Console.WriteLine($"The job failed with an error: {ex}");
        }
    }

    private static async void Job_ProgressUpdated(int progress)  {
        await Task.Delay(100); // just an example - my real code needs to call async methods.
        Console.WriteLine($"The Job is at {progress}%.");
        //***
        //a progress update should not determine if something went wrong
        //***
        //if (progress == 5)
        //throw new Exception("Something went wrong!");
    }

    static void Main(string[] args) {
        MainAsync().GetAwaiter().GetResult();
        Console.WriteLine("Reached the end of the program.");
        Console.ReadKey();
    }
}

修改

您需要考虑从进度更新的事件处理程序或任何async事件处理程序中抛出异常不一定会破坏您的工作。但是,您可以从事件处理程序中取消作业,并像这样捕获OperationCanceledException

public class BackgroundJob {
    public int Progress { get; private set; }
    public event EventHandler ProgressUpdated;
    public async Task Start(CancellationToken token) {
        for (var i = 0; i < 100; i++) {
            token.ThrowIfCancellationRequested();
            await Task.Delay(1000);
            Progress++;
            ProgressUpdated?.Invoke(this, EventArgs.Empty);
        }
    }
}

public class Program {        

    private static CancellationTokenSource cts = new CancellationTokenSource()

    static async Task MainAsync() {
        var job = new BackgroundJob();
        job.ProgressUpdated += Job_ProgressUpdated;
        try {
            await job.Start(cts.Token);
        } catch (OperationCanceledException ex) {
            Console.WriteLine($"The job failed with an error: {ex}");
        }
    }

    private static async void Job_ProgressUpdated(object sender, EventArgs e) {
        var job = (BackgroundJob)sender;
        await Task.Delay(100); // just an example - my real code needs to call async methods.
        Console.WriteLine($"The Job is at {job.Progress}%.");
        if (job.Progress == 5)
            cts.Cancel();
    }

    static void Main(string[] args) {
        MainAsync().GetAwaiter().GetResult();
        Console.WriteLine("Reached the end of the program.");
        Console.ReadKey();
    }
}

答案 1 :(得分:1)

我只是回答你的评论,因为你的问题已经被大部分覆盖了。

  

如果异步void事件处理程序中的异常永远不会被捕获,那么它们是否应该总是包装在一个try / catch中以避免崩溃应用程序?

只要在ds = gdal.Open("path to some file") for band in range( ds.RasterCount ): band += 1 nparray = ds.GetRasterBand(band).ReadAsArray() 方法中抛出异常,就会将其发布到当前同步上下文。大多数情况下,它会导致应用程序崩溃。这就是为什么,是的,async void方法应该在有意义时捕获异常。

也就是说,有一种从async void方法外部捕获异常的hackish方法:构建自定义同步上下文来拦截异常。

async void

我再说一遍:我只是为那些好奇的头脑发帖子。这依赖于未记录的内部机制,这些内部机制可能会在框架的任何更新时中断,并且不应在实际的生产代码中使用。