10

TPLでは、例外が例外によってスローされた場合仕事その例外はキャプチャされ、に保存されます。Task.Exceptionその後、のすべての規則に従います。観察された例外。それが観察されないならば、それは最終的にファイナライザスレッドに投げ直され、プロセスをクラッシュさせます。

タスクがその例外を捕捉して代わりにそれを伝播させるのを防ぐ方法はありますか?

私が興味を持っているタスクはすでにUIスレッド上で実行されているでしょう(提供:TaskScheduler.FromCurrentSynchronizationContextそして、私はそれが私の既存の人によって処理されることができるように例外をエスケープしたいですApplication.ThreadException取引。

基本的には、タスク内の未処理の例外がボタンクリックハンドラ内の未処理の例外のように動作するようにします。ただちにUIスレッドに伝播し、ThreadExceptionによって処理されます。

4 답변


11

Ok Joe ...約束されているとおり、これをカスタムで一般的に解決する方法があります。TaskSchedulerサブクラス私はこの実装をテストしました、そしてそれは魅力のように働きます。忘れないで見たい場合はデバッガをアタッチすることはできませんApplication.ThreadException実際に発砲する!

カスタムTaskScheduler

このカスタムTaskScheduler実装は特定のものに結び付けられています。SynchronizationContext「出生時」に、各着信を受け取りますTaskそれが実行する必要があるということは、論理的なTask障害が発生し、それが発生すると、Posts SynchronizationContextに戻り、そこでSynchronizationContextから例外をスローします。Taskそれは失敗しました。

public sealed class SynchronizationContextFaultPropagatingTaskScheduler : TaskScheduler
{
    #region Fields

    private SynchronizationContext synchronizationContext;
    private ConcurrentQueue<Task> taskQueue = new ConcurrentQueue<Task>();

    #endregion

    #region Constructors

    public SynchronizationContextFaultPropagatingTaskScheduler() : this(SynchronizationContext.Current)
    {
    }

    public SynchronizationContextFaultPropagatingTaskScheduler(SynchronizationContext synchronizationContext)
    {
        this.synchronizationContext = synchronizationContext;
    }

    #endregion

    #region Base class overrides

    protected override void QueueTask(Task task)
    {
        // Add a continuation to the task that will only execute if faulted and then post the exception back to the synchronization context
        task.ContinueWith(antecedent =>
            {
                this.synchronizationContext.Post(sendState =>
                {
                    throw (Exception)sendState;
                },
                antecedent.Exception);
            },
            TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);

        // Enqueue this task
        this.taskQueue.Enqueue(task);

        // Make sure we're processing all queued tasks
        this.EnsureTasksAreBeingExecuted();
    }

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        // Excercise for the reader
        return false;
    }

    protected override IEnumerable<Task> GetScheduledTasks()
    {
        return this.taskQueue.ToArray();
    }

    #endregion

    #region Helper methods

    private void EnsureTasksAreBeingExecuted()
    {
        // Check if there's actually any tasks left at this point as it may have already been picked up by a previously executing thread pool thread (avoids queueing something up to the thread pool that will do nothing)
        if(this.taskQueue.Count > 0)
        {
            ThreadPool.UnsafeQueueUserWorkItem(_ =>
            {
                Task nextTask;

                // This thread pool thread will be used to drain the queue for as long as there are tasks in it
                while(this.taskQueue.TryDequeue(out nextTask))
                {
                    base.TryExecuteTask(nextTask);
                }
            },
            null);
        }
    }

    #endregion
}

この実装に関するいくつかの注意/免責事項:

  • パラメータなしのコンストラクタを使用すると、現在のSynchronizationContext ...が取得されます。したがって、これをWinFormsスレッド(メインフォームコンストラクタなど)で構築するだけで、自動的に機能します。おまけに、他の場所から取得したSynchronizationContextを明示的に渡すことができるコンストラクタもあります。
  • 私はTryExecuteTaskInlineの実装を提供していないので、この実装はただ単にキューに入れるだけです。Task取り組まれる。私はこれを読者のための練習として残しておきます。それは難しいことではありません、ちょうど...あなたが求めている機能を実証する必要はありません。
  • ThreadPoolを利用したタスクのスケジューリング/実行には、単純な/原始的なアプローチを使用しています。確実に豊富な実装がありますが、この実装の焦点は単に例外を「アプリケーション」スレッドにマーシャリングすることだけです。

さて、今、あなたはこのTaskSchedulerを使用するためのいくつかの選択肢があります:

TaskFactoryインスタンスを事前設定する

この方法では、TaskFactoryそのファクトリインスタンスで開始したタスクは、いつでもカスタムを使用します。TaskScheduler。それは基本的に次のようになります。

アプリケーション起動時

private static readonly TaskFactory MyTaskFactory = new TaskFactory(new SynchronizationContextFaultPropagatingTaskScheduler());

コード全体

MyTaskFactory.StartNew(_ =>
{
    // ... task impl here ...
});

明示的TaskScheduler呼び出しごと

もう1つの方法は、カスタムのインスタンスを作成することです。TaskSchedulerそしてそれをに渡すStartNewデフォルトでTaskFactoryタスクを開始するたびに。

アプリケーション起動時

private static readonly SynchronizationContextFaultPropagatingTaskScheduler MyFaultPropagatingTaskScheduler = new SynchronizationContextFaultPropagatingTaskScheduler();

コード全体

Task.Factory.StartNew(_ =>
{
    // ... task impl here ...
},
CancellationToken.None // your specific cancellationtoken here (if any)
TaskCreationOptions.None, // your proper options here
MyFaultPropagatingTaskScheduler);


  • 興味深いアプローチと徹底的な説明のために+1。残念ながら私はUIスレッド上でいくつかのタスク(例えばプログレスバーを更新するだけの暫定タスクなど)を実行することができる必要があるので、これが常にスレッドプール上のタスクをスケジュールするという事実はそれが十分でないことを意味しますそのまま。私はこのアイデアを別のTaskSchedulerをラップするデコレータに適応させることができるのだろうか。 - Joe White
  • @JoeWhite - この場合、彼らはちょっとAPI設計をねじ込みました。そうだった正確に私がQueueTaskの暗黙の中に継続をフックし、それから私が構築した誰かによって提供された内側のTaskSchedulerに委譲するというデコレータアプローチを始めたときの私の意図。残念ながら、TaskScheduler :: QueueTaskは保護されているだけなので、実際にそのように委任する方法はありません。 :(私がこれを達成すると考えることができる唯一の方法はTaskScheduler :: FromCurrentSynchronizationContextがするようにそれらをSynchronizationContext :: Postに委任することによって実行されるタスクをキューに入れるもう一つの暗黙のうちにあるでしょう。 - Drew Marsh

4

私は時々適切に機能する解決策を見つけました。

シングルタスク

var synchronizationContext = SynchronizationContext.Current;
var task = Task.Factory.StartNew(...);

task.ContinueWith(task =>
    synchronizationContext.Post(state => {
        if (!task.IsCanceled)
            task.Wait();
    }, null));

これはへの呼び出しをスケジュールします。task.Wait()UIスレッドで。しないのでWaitタスクが既に完了したことがわかるまで、実際にはブロックされません。例外があるかどうかを確認するだけで、発生した場合はスローされます。以来SynchronizationContext.Postコールバックはメッセージループから直接実行されます。Task)、TPLは例外を停止せず、ボタンクリックハンドラで未処理の例外であるかのように、正常に伝播できます。

もう1つのしわは、電話したくないということです。WaitAllタスクがキャンセルされた場合キャンセルされたタスクを待つと、TPLはTaskCanceledExceptionこれは再スローしても意味がありません。

複数のタスク

私の実際のコードでは、最初のタスクと複数の継続という複数のタスクがあります。それらのうちのどれか(潜在的に複数)が例外を受けた場合、私はAggregateExceptionUIスレッドに戻ります。これを処理する方法は次のとおりです。

var synchronizationContext = SynchronizationContext.Current;
var firstTask = Task.Factory.StartNew(...);
var secondTask = firstTask.ContinueWith(...);
var thirdTask = secondTask.ContinueWith(...);

Task.Factory.ContinueWhenAll(
    new[] { firstTask, secondTask, thirdTask },
    tasks => synchronizationContext.Post(state =>
        Task.WaitAll(tasks.Where(task => !task.IsCanceled).ToArray()), null));

同じ話:すべてのタスクが完了したら、電話してくださいWaitAllのコンテキスト外Task。タスクはすでに完了しているので、ブロックされることはありません。投げるのは簡単な方法ですAggregateExceptionいずれかのタスクが失敗した場合

最初のうちは、継続タスクの1つが次のようなものを使用しているのではないかと心配しました。TaskContinuationOptions.OnlyOnRanToCompletionそして最初のタスクが失敗し、そしてWaitAllコールがハングアップする可能性があります(継続タスクが実行されないため、私は心配しました)。WaitAllそれが実行されるのを待つのをブロックします)。しかし、それはTPL設計者がそれより賢いということがわかります - もし継続タスクが実行されないならば。OnlyOnまたはNotOnフラグ、継続タスクがCanceled状態、従ってそれは妨げませんWaitAll

編集する

マルチタスクバージョンを使用すると、WaitAll呼び出しがスローされますAggregateException、 でもあのAggregateExceptionにそれを通らないThreadExceptionhandler:代わりにのみ1その内部例外のうちのThreadException。そのため、複数のタスクが例外をスローした場合、そのうちの1つだけがスレッド例外ハンドラに到達します。これがなぜなのかはっきりしませんが、把握しようとしています。


  • これはうまくいきますが、アプリケーション内のすべてのタスクに継続を強制する必要があります。それがあなたにとって受け入れられる解決策であることに気づかなかったので、もっと一般的な解決策を探していました。私はカスタムTaskSchedulerを使って一般的にそれをする方法があると思います、そして今朝その解決策をテスト/投稿するつもりでした。まだその用語に興味があり、それでも問題が解決しない場合は、この解決策がうまくいくことを嬉しく思います。 - Drew Marsh
  • @DrewMarsh、私が最初に望んでいたのと同じように私のアプローチはうまくいきません(私の答えの最新の編集を見てください)。ご指摘のとおり、毎回手動で接続する必要があります。だから私は特にこの解決策にこだわっていない - もしあなたが他のアイデアを持っているならば、私はそれらを聞くのが大好きだ。 - Joe White
  • 今考えている問題は、カスタムのTaskSchedulerを使っていても、やらなければならないことです。つかいますどこでもそのカスタムTaskScheduler。継続をすべてに平手打ちする必要があるよりも軽量ですが、それでもコードのすべての領域を変更する必要があります。また、タスクを返したサードパーティのメソッドを呼び出している場合でも、サードパーティのライブラリがタスクの作成にTaskSchedulerを使用することは保証されていないため、継続してそこでWait()を実行する必要があります。実際には非常にありそうもない)。うーん...トリッキー。 - Drew Marsh
  • それが価値があるもののために、我々は実際に意志すでにすべてが継続されています(タスクを開始するとUIは無効になり、終了したら再度有効にする必要があります。これはUIスレッドで実行される継続を意味します)。ただし、サードパーティのメソッドからタスクを取得することはできないため、特にすべてのタスクではないという問題を解決できる場合は、TaskSchedulerを使用することをお勧めします。例外が発生します。 - Joe White
  • Joeさん、かっこいいです。今日は昼食時にサンプルを一緒に試してみましょう。 - Drew Marsh

0

これらの例外がメインスレッドからの例外のように伝播することを認識している方法はありません。ちょうどあなたがにフックしているのと同じハンドラをフックしないでくださいApplication.ThreadExceptionTaskScheduler.UnobservedTaskException同様に?


  • UnobservedTaskExceptionは、タスクが例外を監視せずにガベージコレクトされようとしているときにのみ発生すると思いました。もしそうなら、それは適用されません - 私はすぐに例外を伝播したい。 - Joe White
  • すみません、ジョー、それがあなたが探していたものだと理解できませんでした。 UnobservedTaskExceptionがタスクのクリーンアップの後まで延期されているというあなたの理解は正しいです。これについてもっと考えさせてください。 - Drew Marsh

0

このスーツのようなものはありますか?

public static async void Await(this Task task, Action action = null)
{
   await task;
   if (action != null)
      action();
}

runningTask.Await();

リンクされた質問


関連する質問

最近の質問