development

TaskScheduler.Current가 기본 TaskScheduler 인 이유는 무엇입니까?

big-blog 2020. 12. 9. 21:08
반응형

TaskScheduler.Current가 기본 TaskScheduler 인 이유는 무엇입니까?


Task Parallel Library는 훌륭하며 지난 몇 달 동안 많이 사용했습니다. 그러나 실제로 나를 괴롭히는 것이 있습니다. TaskScheduler.Current기본 작업 스케줄러가 아니라 TaskScheduler.Default. 이것은 문서 나 샘플에서 언뜻보기에 절대적으로 명확하지 않습니다.

Current다른 작업 안에 있는지 여부에 따라 동작이 변경되기 때문에 미묘한 버그가 발생할 수 있습니다. 쉽게 결정할 수 없습니다.

.NET Framework에서 XxxAsync 메서드가 수행하는 것과 똑같은 방식으로 이벤트를 기반으로하는 표준 비동기 패턴을 사용하여 원래 동기화 컨텍스트에서 완료를 알리는 비동기 메서드 라이브러리를 작성한다고 가정합니다 DownloadFileAsync. 다음 코드로이 동작을 구현하는 것이 정말 쉽기 때문에 구현을 위해 Task Parallel Library를 사용하기로 결정했습니다.

public class MyLibrary {
    public event EventHandler SomeOperationCompleted;

    private void OnSomeOperationCompleted() {
        var handler = SomeOperationCompleted;
        if (handler != null)
            handler(this, EventArgs.Empty);
    }

    public void DoSomeOperationAsync() {
                    Task.Factory
                        .StartNew
                         (
                            () => Thread.Sleep(1000) // simulate a long operation
                            , CancellationToken.None
                            , TaskCreationOptions.None
                            , TaskScheduler.Default
                          )
                        .ContinueWith
                           (t => OnSomeOperationCompleted()
                            , TaskScheduler.FromCurrentSynchronizationContext()
                            );
    }
}

지금까지 모든 것이 잘 작동합니다. 이제 WPF 또는 WinForms 애플리케이션에서 버튼 클릭으로이 라이브러리를 호출 해 보겠습니다.

private void Button_OnClick(object sender, EventArgs args) {
    var myLibrary = new MyLibrary();
    myLibrary.SomeOperationCompleted += (s, e) => DoSomethingElse();
    myLibrary.DoSomeOperationAsync();
}

private void DoSomethingElse() {
    ...
    Task.Factory.StartNew(() => Thread.Sleep(5000)/*simulate a long operation*/);
    ...
}

여기서 라이브러리 호출을 작성하는 사람 Task은 작업이 완료되면 새로 시작하도록 선택했습니다 . 특이한 것은 없습니다. 그 또는 그녀는 웹의 모든 곳에서 발견되는 예제를 따르고 Task.Factory.StartNew지정하지 않고 간단히 사용 합니다 TaskScheduler(두 번째 매개 변수에서 지정하는 쉬운 오버로드가 없습니다). DoSomethingElse메서드는 단독으로 호출 될 때 제대로 작동하지만 이벤트에 의해 호출되는 즉시 TaskFactory.Current내 라이브러리 연속에서 동기화 컨텍스트 작업 스케줄러를 재사용 하므로 UI가 정지 됩니다.

특히 두 번째 작업 호출이 복잡한 호출 스택에 묻혀있는 경우이를 알아내는 데 시간이 걸릴 수 있습니다. 물론 모든 것이 어떻게 작동하는지 알면 여기서 수정하는 것은 간단 TaskScheduler.Default합니다. 스레드 풀에서 실행될 것으로 예상되는 작업에 대해 항상 지정 하십시오. 그러나 두 번째 작업은 다른 외부 라이브러리에서 시작되어이 동작에 대해 알지 못하고 StartNew특정 스케줄러없이 순진하게 사용 합니다. 나는이 사건이 매우 흔할 것으로 예상하고 있습니다.

머리를 감은 후 TPL을 작성하는 팀 이 기본값 TaskScheduler.Current대신 사용할 선택을 이해할 수 없습니다 TaskScheduler.Default.

  • 전혀 명확 Default하지 않으며 기본값이 아닙니다! 그리고 문서는 심각하게 부족합니다.
  • 에서 사용하는 실제 작업 스케줄러 Current는 호출 스택 따라 다릅니다! 이 동작으로 불변성을 유지하는 것은 어렵습니다.
  • StartNew작업 생성 옵션과 취소 토큰을 먼저 지정해야하기 때문에 작업 스케줄러를 지정하는 것은 번거롭고 , 길고 읽기 어려운 줄로 이어집니다. 이것은 확장 메소드를 작성 또는 생성함으로써 완화 될 수있다 TaskFactory그 용도 Default.
  • 호출 스택 캡처에는 추가 성능 비용이 있습니다.
  • 작업이 다른 부모 실행 작업에 종속되기를 원할 때 호출 스택 마법에 의존하는 것보다 코드 읽기를 쉽게하기 위해 명시 적으로 지정하는 것을 선호합니다.

나는이 질문이 매우 주관적으로 들릴 수 있다는 것을 알고 있지만,이 행동이 왜 그런지에 대해 좋은 객관적인 주장을 찾을 수 없습니다. 나는 여기서 뭔가를 놓치고 있다고 확신한다. 그것이 내가 당신에게 의지하는 이유이다.


현재의 행동이 타당하다고 생각합니다. 나만의 작업 스케줄러를 만들고 다른 작업을 시작하는 작업을 시작하면 모든 작업이 내가 만든 스케줄러를 사용하기를 원할 것입니다.

가끔 UI 스레드에서 작업을 시작하는 것이 기본 스케줄러를 사용하고 때로는 사용하지 않는 것이 이상하다는 데 동의합니다. 하지만 내가 그것을 디자인한다면 어떻게 더 좋게 만들 수 있을지 모르겠습니다.

특정 문제와 관련하여 :

  • 지정된 스케줄러에서 새 작업을 시작하는 가장 쉬운 방법은 new Task(lambda).Start(scheduler). 이것은 작업이 무언가를 반환하는 경우 형식 인수를 지정해야한다는 단점이 있습니다. TaskFactory.Create유형을 추론 할 수 있습니다.
  • 을 사용하는 Dispatcher.Invoke()대신 사용할 수 있습니다 TaskScheduler.FromCurrentSynchronizationContext().

[편집] 다음은에서 사용하는 스케줄러의 문제 만 해결합니다 Task.Factory.StartNew.
그러나 Task.ContinueWith하드 코딩 된 TaskScheduler.Current. [/편집하다]

첫째, 사용 가능한 쉬운 솔루션이 있습니다.이 게시물의 하단을 참조하십시오.

이 문제 뒤에 이유는 간단하다 : 기본 작업 스케줄러 (뿐만 아니라이 TaskScheduler.Default)뿐만 아니라에 대한 기본 작업 스케줄러는 TaskFactory( TaskFactory.Scheduler). 이 기본 스케줄러는 TaskFactory생성 될 때 의 생성자에서 지정할 수 있습니다 .

그러나 TaskFactory뒤에는 Task.Factory다음과 같이 생성됩니다.

s_factory = new TaskFactory();

As you can see, no TaskFactory is specified; null is used for the default constructor - better would be TaskScheduler.Default (the documentation states that "Current" is used which has the same consequences).
This again leads to the implementation of TaskFactory.DefaultScheduler (a private member):

private TaskScheduler DefaultScheduler 
{ 
   get
   { 
      if (m_defaultScheduler == null) return TaskScheduler.Current;
      else return m_defaultScheduler;
   }
}

Here you should see be able to recognize the reason for this behaviour: As Task.Factory has no default task scheduler, the current one will be used.

So why don't we run into NullReferenceExceptions then, when no Task is currently executing (i.e. we have no current TaskScheduler)?
The reason is simple:

public static TaskScheduler Current
{
    get
    {
        Task internalCurrent = Task.InternalCurrent;
        if (internalCurrent != null)
        {
            return internalCurrent.ExecutingTaskScheduler;
        }
        return Default;
    }
}

TaskScheduler.Current defaults to TaskScheduler.Default.

I would call this a very unfortunate implementation.

However, there is an easy fix available: We can simply set the default TaskScheduler of Task.Factory to TaskScheduler.Default

TaskFactory factory = Task.Factory;
factory.GetType().InvokeMember("m_defaultScheduler", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, null, factory, new object[] { TaskScheduler.Default });

I hope I could help with my response although it's quite late :-)


Instead of Task.Factory.StartNew()

consider using: Task.Run()

This will always execute on a thread pool thread. I just had the same problem described in the question and I think that is a good way of handling this.

See this blog entry: http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx


It's not obvious at all, Default is not the default! And the documentation is seriously lacking.

Default is the default, but it's not always the Current.

As others have already answered, if you want a task to run on the thread pool, you need to explicitly set the Current scheduler by passing the Default scheduler into either the TaskFactory or the StartNew method.

Since your question involved a library though, I think the answer is that you should not do anything that will change the Current scheduler that's seen by code outside your library. That means that you should not use TaskScheduler.FromCurrentSynchronizationContext() when you raise the SomeOperationCompleted event. Instead, do something like this:

public void DoSomeOperationAsync() {
    var context = SynchronizationContext.Current;
    Task.Factory
        .StartNew(() => Thread.Sleep(1000) /* simulate a long operation */)
        .ContinueWith(t => {
            context.Post(_ => OnSomeOperationCompleted(), null);
        });
}

I don't even think you need to explicitly start your task on the Default scheduler - let the caller determine the Current scheduler if they want to.


I've just spent hours trying to debug a weird issue where my task was scheduled on the UI thread, even though I didn't specify it to. It turned out the problem was exactly what your sample code demonstrated: A task continuation was scheduled on the UI thread, and somewhere in that continuation, a new task was started which then got scheduled on the UI thread, because the currently executing task had a specific TaskScheduler set.

Luckily, it's all code I own, so I can fix it by making sure my code specify TaskScheduler.Default when starting new tasks, but if you aren't so lucky, my suggestion would be to use Dispatcher.BeginInvoke instead of using the UI scheduler.

So, instead of:

var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
var task = Task.Factory.StartNew(() => Thread.Sleep(5000));
task.ContinueWith((t) => UpdateUI(), uiScheduler);

Try:

var uiDispatcher = Dispatcher.CurrentDispatcher;
var task = Task.Factory.StartNew(() => Thread.Sleep(5000));
task.ContinueWith((t) => uiDispatcher.BeginInvoke(new Action(() => UpdateUI())));

It's a bit less readable though.

참고URL : https://stackoverflow.com/questions/6800705/why-is-taskscheduler-current-the-default-taskscheduler

반응형