2017-05-01 9 views
0

私は、ジョブを非同期で実行し、進行が行われるたびにイベントを発生させる「バックグラウンドジョブ」クラスを持っています。このイベントのイベントハンドラで例外が発生した場合、呼び出し元のメソッドで捕捉されることはありません。非同期イベントハンドラから例外をキャッチ

イベントハンドラを非同期に切り替えるとどうなりますか。問題は解消されます。しかし、その後、イベントハンドラから非同期呼び出しをすべて呼び出しをブロックするようにしなければならず、そうする必要はありません。

非同期イベントハンドラから発生した例外をキャッチする方法はありますか?

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

あなたは間違った質問をするように導かれました。進捗レポートはObserverパターンの実装です。イベントソースでハンドラ例外を捕まえることは決して必要ありません。あなたがObserverパターンを使用しているのではなく、何か他のものを使用していることを示す必要があるという事実。たとえば、テンプレートメソッドはイベントを使用して誤って実装されることがあります。 –

+0

@StephenClearyそれは非常に良い点です。私は確かにあなたが示唆しているものの有罪と私はかなり私はこの問題を避けるために私はJStewardから得たヒントを使用して書き換えることができると確信しています。さて、それでは、個人的な好奇心のためだけに、イベント処理コードから例外がスローされる可能性はありませんか?そして、非同期voidイベントハンドラからの例外を捕らえることができない場合、アプリケーションをクラッシュさせないために、try/catchのブランケットに常にラップするべきではありませんか?ありがとう! –

+1

'async void'は、(トップレベルの)UIイベントハンドラをエミュレートします。 UIイベントが例外を発生させると、それはメッセージループにまっすぐ進むので、 'async void'メソッドは' SynchronizationContext'で直接例外を発生させることでその動作をエミュレートします。したがって、あなたのUIイベントハンドラでtry/catchが必要な場合は、 'async void'メソッドで必要になります。例外を捕まえることを可能にするいくつかの回避策があります( 'Task'を返すイベントのように)が、再設計が最善のアプローチです。 –

答えて

2

async void方法はasync法の範囲内である必要があり、その捕獲文脈あなたtry catch上での例外がスローされます。さらに、古いBackgroundWorkerのようなあなたのやっていることは大変よく聞こえますが、ホイールを再構築しないで、進捗状況を報告しているasyncのジョブを実行するためのオプションが既に用意されています。 asyncの進行状況の更新についてはhereを見てください。 、あなたは進捗更新イベントハンドラ、または任意のasyncイベントハンドラから例外をスローすることを検討する必要がある

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 awaitの利点と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(); 
    } 
} 
+2

イベントハンドラは例外を呑み込まないでしょう - たとえ 'async void'でも例外をスローします – Fabio

+0

@Fabioはこれに感謝して、今朝自分自身に先んじています。 – JSteward

+0

@JSteward私がコードサンプルで本当に求めているのは、非同期voidイベントハンドラからスローされた例外を処理する方法です。すべての非同期イベントハンドラが何かを発生させると仮定すると、すべての非同期voidイベントハンドラはブランケットに包まれるべきですtry ... catch? あなたが提供したTAPリンクをチェックします。ありがとう! –

1

私はちょうどあなたの質問のほとんどはすでに覆われているとして、あなたのコメントにお答えするつもりです。

そして、非同期voidイベントハンドラからの例外を捕まえることができない場合、アプリケーションをクラッシュさせることを避けるためにtry/catchブランケットで常にラップするべきではありませんか?

async voidメソッドで例外がスローされると、そのメッセージは現在の同期コンテキストに送信されます。ほとんどの場合、アプリケーションがクラッシュします。だからこそ、はい、async voidのメソッドは、これが意味を成すときはいつでも例外をキャッチする必要があります。

つまり、async voidメソッドの外部から例外をキャッチするためのヒントがあります。つまり、例外を傍受するカスタム同期コンテキストを構築することです。

public static void Test() 
{ 
    throw new Exception("Synchronous"); 
} 

public static async void TestAsync() 
{ 
    await Task.Yield(); 

    throw new Exception("Asynchronous"); 
} 

public class EventSynchronizationContext : SynchronizationContext 
{ 
    public override void Post(SendOrPostCallback d, object state) 
    { 
     if (state is ExceptionDispatchInfo 
      && d.Target.GetType().ReflectedType.FullName == "System.Runtime.CompilerServices.AsyncMethodBuilderCore") 
     { 
      // Caught an exception 
      var exceptionInfo = (ExceptionDispatchInfo)state; 
      Console.WriteLine("Caught asynchronous exception: " + exceptionInfo.SourceException); 

      return; 
     } 

     base.Post(d, state); 
    } 
} 

static void Main(string[] args) 
{ 
    SomeEvent += TestAsync; 
    SomeEvent += Test; 

    var previousSynchronizationContext = SynchronizationContext.Current; 

    try 
    { 
     SynchronizationContext.SetSynchronizationContext(new EventSynchronizationContext()); 

     SomeEvent(); 
    } 
    catch (Exception ex) 
    { 
     Console.WriteLine("Caught synchronous exception: " + ex); 
    } 
    finally 
    { 
     SynchronizationContext.SetSynchronizationContext(previousSynchronizationContext); 
    } 

    Console.ReadLine(); 
} 

私はこれをもう一度言っています:私は好奇心のためにそれを掲示しています。これは、文書化された内部力学に依存しており、フレームワークの更新時に破損する可能性があり、実際の生産コードでは使用しないでください。

関連する問題