10

TPL에서 예외가 Throw되면태스크, 그 예외는 캡쳐되어Task.Exception, 다음에있는 모든 규칙을 따릅니다.관측 예외. 결코 관찰되지 않는다면, 결국 파이널 라이저 스레드에서 다시 발생하고 프로세스를 중단합니다.

태스크가 해당 예외를 포착하지 못하도록 방지하는 방법이 있습니까?

관심있는 작업은 UI 스레드에서 이미 실행되고있을 것입니다 (TaskScheduler.FromCurrentSynchronizationContext), 예외를 이스케이프 처리하여 기존의 내 처리 방법으로 처리 할 수있게하려고합니다.Application.ThreadException거래.

나는 기본적으로 Button-Click 핸들러에서 처리되지 않은 예외처럼 동작하는 Task에서 처리되지 않은 예외를 원합니다. UI 스레드에서 즉시 전파되고 ThreadException에 의해 처리됩니다.

4 답변


11

Ok Joe ... 약속대로이 문제를 일반적으로 사용자 정의로 해결할 수 있습니다.TaskScheduler아강. 이 구현을 테스트 한 결과 매력처럼 작동합니다.잊지 마라.보고 싶으면 디버거를 연결할 수 없습니다.Application.ThreadException실제로 발사!

사용자 지정 TaskScheduler

이 사용자 정의 TaskScheduler 구현은 특정SynchronizationContext"출생"시에Task그것을 실행해야 할 필요가있다.Task결함이 발생하면 그 불이PostSynchronizationContext로 돌아가서 어디서 예외를 던질 것인가?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을 활용하는 Tasks를 스케줄링 / 실행하기위한 단순한 / 원시적인 접근법을 사용하고있다. 확실히 풍부한 구현이 있지만,이 구현의 초점은 단순히 "응용 프로그램"스레드로 다시 예외를 마샬링하는 것입니다.

이제 TaskScheduler를 사용하기위한 몇 가지 옵션이 있습니다.

사전 구성 TaskFactory 인스턴스

이 방법을 사용하면TaskFactory한 번 누른 다음 해당 팩토리 인스턴스로 시작하는 모든 작업은TaskScheduler. 기본적으로 다음과 같이 보입니다.

응용 프로그램 시작시

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

전체 코드

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

명시 적 TaskScheduler 통화 별

또 다른 방법은 사용자 지정 인스턴스를 만드는 것입니다.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 impl에서 연속을 연결 한 다음 누구든지 나를 구성한 내부 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은 예외를 멈추지 않고 버튼 클릭 처리기에서 처리되지 않은 예외 인 것처럼 정상적으로 전파 할 수 있습니다.

하나의 여분의 주름은 제가 전화하고 싶지 않다는 것입니다.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실패한 태스크가있는 경우

처음에 나는 계속 작업 중 하나가 다음과 같은 것을 사용했다면 걱정했다.TaskContinuationOptions.OnlyOnRanToCompletion, 첫 번째 작업이 오류가 발생하면WaitAll통화가 끊어 질 수 있습니다 (연속 작업이 실행되지 않으므로,WaitAll실행을 기다리는 것을 차단합니다). 그러나 TPL 설계자는 그보다 더 개연성이있었습니다. 왜냐하면 계속 작업이OnlyOn또는NotOn플래그는 연속 작업이Canceled상태를 차단하지 않습니다.WaitAll.

편집하다

다중 작업 버전을 사용할 때WaitAll호출은AggregateException,하지만 그건AggregateException그것으로 끝나지 않는다.ThreadException처리기 : 대신하나내부 예외가 전달됩니다.ThreadException. 따라서 여러 작업에서 예외가 발생하면 그 중 하나만 스레드 예외 처리기에 도달합니다. 왜 그런지는 확실치 않지만, 알아 내려고하고 있습니다.


  • 음,이 작동하지만 응용 프로그램의 모든 작업을 계속 진행해야합니다. 이것이 내가 받아 들일 수있는 해결책이라는 것을 깨닫지 못했고 좀 더 일반적인 해결책을 찾고있었습니다. 나는 사용자 정의 TaskScheduler로 총체적으로 할 수있는 방법이 있다고 생각하고 오늘 아침에 그 솔루션을 테스트 / 게시 할 예정이었다. 해당 부분에 계속 관심이 있으시면 계속 노력하십시오. 그렇지 않으면이 솔루션이 도움이되기 때문에 기쁩니다. - Drew Marsh
  • @DrewMarsh, 내 접근 방식은 처음에는 기대했던 것뿐 아니라 (내 답변에 대한 최신 편집 참조); 당신이 지적했듯이 매번 수동으로 연결해야합니다. 따라서 특별히이 솔루션에 첨부되지 않았습니다. 다른 아이디어가 있으면 듣고 싶습니다. - Joe White
  • 지금 생각해 보면 문제는 사용자 지정 TaskScheduler를 사용하는 경우에도용도그 사용자 정의 TaskScheduler 어디서나. 연속을 모든 것에 적용하는 것보다 가볍지 만 코드의 모든 영역을 변경해야합니다. 또한 작업을 반환 한 타사 메서드를 호출하는 경우 타사 라이브러리에서 TaskScheduler를 사용하여 작업을 생성한다는 보장이 없으므로 계속 작업을 계속하고 Wait ()에 연결해야합니다. 매우 가능성은 거의 없음). 흠 ... 까다 롭습니다. - Drew Marsh
  • 그게 무엇을 위해, 우리는 실제로의지이미 모든 작업을 계속 진행하고 있습니다. 작업을 시작할 때 UI를 사용하지 않도록 설정 했으므로 작업이 끝나면 다시 활성화해야합니다. UI 스레드에서 계속 실행됩니다. 하지만 타사 방식으로는 작업을 가져올 수 없으므로 특히 TaskScheduler가 모든 작업이 아닌 문제를 해결할 수 있다면 가능할 것입니다. 예외가 발생합니다. - Joe White
  • Joe, ok, 오늘 점심 시간에 샘플을 함께 채찍으로 채워 보겠습니다. - Drew Marsh

0

이러한 예외가 주 스레드의 예외처럼 전파되는 것을 인식하고있는 방법은 없습니다. 왜 당신이 연결하고있는 핸들러에 연결하지 않는 것일까 요?Application.ThreadExceptionTaskScheduler.UnobservedTaskException게다가?


  • UnobservedTaskException은 태스크가 관찰 된 예외없이 가비지 수집하려고 할 때만 발생한다고 생각했습니다. 그렇다면 적용되지 않습니다. 즉시 예외를 전파하고 싶습니다. - Joe White
  • 죄송합니다 Joe, 당신이 찾고있는 것을 이해하지 못했습니다. UnobservedTaskException에 대한 이해가 작업 정리 후 지연됩니다. 이것에 대해 더 생각해 보도록하겠습니다. - Drew Marsh

0

이 양복 같은 것이 있습니까?

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

runningTask.Await();

연결된 질문


관련된 질문

최근 질문