Why is TaskScheduler.Current the default TaskScheduler?

后端 未结 5 672
野趣味
野趣味 2020-11-30 20:25

The Task Parallel Library is great and I\'ve used it a lot in the past months. However, there\'s something really bothering me: the fact that TaskScheduler.Current is the de

相关标签:
5条回答
  • 2020-11-30 20:40

    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

    0 讨论(0)
  • 2020-11-30 20:43

    [EDIT] The following only addresses the problem with the scheduler used by Task.Factory.StartNew.
    However, Task.ContinueWith has a hardcoded TaskScheduler.Current. [/EDIT]

    First, there is an easy solution available - see the bottom of this post.

    The reason behind this problem is simple: There is not only a default task scheduler (TaskScheduler.Default) but also a default task scheduler for a TaskFactory (TaskFactory.Scheduler). This default scheduler can be specified in the constructor of the TaskFactory when it's created.

    However, the TaskFactory behind Task.Factory is created as follows:

    s_factory = new TaskFactory();
    

    As you can see, no TaskScheduler 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 :-)

    0 讨论(0)
  • 2020-11-30 20:48

    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.

    0 讨论(0)
  • 2020-11-30 20:53

    I think the current behavior makes sense. If I create my own task scheduler, and start some task that starts other tasks, I probably want all the tasks to use the scheduler I created.

    I agree that it's odd that sometimes starting a task from the UI thread uses the default scheduler and sometimes not. But I don't know how would I make this better if I was designing it.

    Regarding your specific problems:

    • I think the easiest way to start a new task on a specified scheduler is new Task(lambda).Start(scheduler). This has the disadvantage that you have to specify type argument if the task returns something. TaskFactory.Create can infer the type for you.
    • You can use Dispatcher.Invoke() instead of using TaskScheduler.FromCurrentSynchronizationContext().
    0 讨论(0)
  • 2020-11-30 21:02

    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.

    0 讨论(0)
提交回复
热议问题