Code Review: Tworzenie wątków za pomocą Task.Factory.StartNew i ContinueWith

Załóżmy, że nie możemy korzystać z await\async i gdzieś w kodzie mamy następujące wywołania:

public MainWindow()
{
  InitializeComponent();
  Task.Factory.StartNew(LoadData).ContinueWith(UpdateUserInterface, TaskScheduler.FromCurrentSynchronizationContext());
}
private static void UpdateUserInterface(Task obj)
{
  Console.WriteLine("Interfejs zaktualizowany.");
  Task task = Task.Factory.StartNew(AnotherTimeConsumingOperation);
}

private static void AnotherTimeConsumingOperation()
{
  // ???
}

private static void LoadData()
{
  Thread.Sleep(5000);
}

Chcemy wykonać operacje za pomocą Task’ow. LoadData to operacja czasochłonna i wykonujemy ją w wątku z puli. Następnie chcemy zaktualizować interfejs użytkownika więc wywołujemy UpdateUserInterface na wątku głównym UI. Na końcu chcemy wykonać kolejną operację więc standardowo wywołujemy StartNew:

Task.Factory.StartNew(AnotherTimeConsumingOperation);

Niestety, jeśli zajrzyjmy do debuggera, okaże się, że ta metoda jest wykonywana na wątku UI!

image

Dlaczego? W końcu spodziewalibyśmy się, że zostanie ściągnięty wątek z puli, ponieważ nie użyliśmy żadnej flagi mówiącej o synchronizacji (tak jak w poprzednim przykładzie).

Zajrzyjmy do Reflector’a:

[MethodImpl(MethodImplOptions.NoInlining), __DynamicallyInvokable]
public unsafe Task StartNew(Action action)
{
    StackCrawlMark mark;
    Task task;
    mark = 1;
    task = Task.InternalCurrent;
    return Task.InternalStartNew(task, action, null, this.m_defaultCancellationToken, this.GetDefaultScheduler(task), this.m_defaultCreationOptions, 0, &mark);
}

 

Widać, że jest używana metoda GetDefaultScheduler do uzyskania scheduler’a:

private TaskScheduler GetDefaultScheduler(Task currTask)
{
    if (this.m_defaultScheduler == null)
    {
        goto Label_000F;
    }
    return this.m_defaultScheduler;
Label_000F:
    if (currTask == null)
    {
        goto Label_0024;
    }
    if ((currTask.CreationOptions & 0x10) != null)
    {
        goto Label_0024;
    }
    return currTask.ExecutingTaskScheduler;
Label_0024:
    return TaskScheduler.Default;
}
  private TaskScheduler GetDefaultScheduler(Task currTask)
{
    if (this.m_defaultScheduler == null)
    {
        goto Label_000F;
    }
    return this.m_defaultScheduler;
Label_000F:
    if (currTask == null)
    {
        goto Label_0024;
    }
    if ((currTask.CreationOptions & 0x10) != null)
    {
        goto Label_0024;
    }
    return currTask.ExecutingTaskScheduler;
Label_0024:
    return TaskScheduler.Default;
}

 

Łatwo sprawdzić, że defaultscheduler będzie NULL więc pierwszy IF nie zostanie wykonany.

Okazuje się, że w naszym przypadku zostanie użyty aktualny scheduler, którym po wywołaniu UpdateUserInterface będzie SynchronizationContextScheduler. Zatem wszystkie wątki od czasu gdzie przekazaliśmy jako scheduler TaskScheduler.FromCurrentSynchronizationContext() będą wykonywane domyślnie na wątku UI.

Rozwiązanie to oczywiście przekazywanie zawsze schedulera jawnie:

Task.Factory.StartNew(AnotherTimeConsumingOperation,new CancellationToken(),TaskCreationOptions.None,TaskScheduler.Default)

Innymi słowy, jeśli nie przekażemy schedulera jawnie to jest używany TaskScheduler.Current a nie TaskScheduler.Default. Z tego względu, zawsze należy jawnie przekazywać taki parametr ponieważ łatwo popełnić błąd. Myślę, że nie jest to dla większości programistów zbyt intuicyjne. Szczególnie, gdy nie mamy jednego łańcucha wywołań a tworzymy wątki w różnym miejscach.

Leave a Reply

Your email address will not be published.