Продолжение задачи в потоке пользовательского интерфейса

214

Существует ли «стандартный» способ указать, что продолжение задачи должно выполняться в потоке, из которого была создана первоначальная задача?

В настоящее время у меня есть код ниже - он работает, но отслеживание диспетчера и создание второго действия кажется ненужными накладными расходами.

dispatcher = Dispatcher.CurrentDispatcher;
Task task = Task.Factory.StartNew(() =>
{
    DoLongRunningWork();
});

Task UITask= task.ContinueWith(() =>
{
    dispatcher.Invoke(new Action(() =>
    {
        this.TextBlock1.Text = "Complete"; 
    }
});
Грег Сансом
источник
В случае вашего примера вы можете использовать Control.Invoke(Action), т.е. TextBlock1.Invokeа неdispatcher.Invoke
полковник Паник
2
Спасибо @ColonelPanic, но я использовал WPF (с тегами), а не winforms.
Грег Сэнсом

Ответы:

352

Назовите продолжение с TaskScheduler.FromCurrentSynchronizationContext():

    Task UITask= task.ContinueWith(() =>
    {
     this.TextBlock1.Text = "Complete"; 
    }, TaskScheduler.FromCurrentSynchronizationContext());

Это подходит, только если текущий контекст выполнения находится в потоке пользовательского интерфейса.

Грег Сансом
источник
40
Это допустимо, только если текущий контекст выполнения находится в потоке пользовательского интерфейса. Если вы поместите этот код в другую задачу, то получите InvalidOperationException (см. Раздел « Исключения »)
stukselbax
3
В .NET 4.5 ответ Йохана Ларссона должен использоваться в качестве стандартного способа продолжения задачи в потоке пользовательского интерфейса. Просто напишите: ждите Task.Run (DoLongRunningWork); this.TextBlock1.Text = "Complete"; Смотрите также: blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
Марсель В.
1
Спасибо за спасение моей жизни. Я трачу часы, чтобы выяснить, как вызывать вещи основного потока в пределах await / ContinueWith. Для всех остальных, как использовать Google Firebase SDK для Unity и все еще есть те же проблемы, это рабочий подход.
CHaP
2
@MarcelW - awaitхороший шаблон - но только если вы находитесь внутри asyncконтекста (такого как объявленный метод async). Если нет, то все равно необходимо сделать что-то вроде этого ответа.
ToolmakerSteve
33

С помощью async вы просто делаете:

await Task.Run(() => do some stuff);
// continue doing stuff on the same context as before.
// while it is the default it is nice to be explicit about it with:
await Task.Run(() => do some stuff).ConfigureAwait(true);

Тем не мение:

await Task.Run(() => do some stuff).ConfigureAwait(false);
// continue doing stuff on the same thread as the task finished on.
Йохан Ларссон
источник
2
Комментарий по falseверсии меня смущает. Я думал, falseзначит, это может продолжаться в другом потоке.
ToolmakerSteve
1
@ToolmakerSteve Зависит от того, какой поток вы думаете. Рабочий поток, используемый Task.Run, или поток вызывающего? Помните, что «тот же поток, на котором завершена задача» означает рабочий поток (избегая «переключения» между потоками). Кроме того, ConfigureAwait (true) не гарантирует, что элемент управления вернется в один и тот же поток , только в один и тот же контекст (хотя различие может быть несущественным).
Макс Барракло
@MaxBarraclough - Спасибо, я неправильно прочитал, что подразумевалось под "той же веткой". избегая переключения между потоками в смысле максимизации производительности за счет использования любого потока, который выполняется [для выполнения задачи «сделать что-то» »], это проясняет это для меня.
ToolmakerSteve
1
Вопрос не указывает на нахождение внутри asyncметода (который необходим, чтобы использовать await). Какой ответ, когда awaitнет в наличии?
ToolmakerSteve
22

Если у вас есть возвращаемое значение, которое вам нужно отправить в пользовательский интерфейс, вы можете использовать общую версию, например:

В моем случае это вызывается из MVVM ViewModel.

var updateManifest = Task<ShippingManifest>.Run(() =>
    {
        Thread.Sleep(5000);  // prove it's really working!

        // GenerateManifest calls service and returns 'ShippingManifest' object 
        return GenerateManifest();  
    })

    .ContinueWith(manifest =>
    {
        // MVVM property
        this.ShippingManifest = manifest.Result;

        // or if you are not using MVVM...
        // txtShippingManifest.Text = manifest.Result.ToString();    

        System.Diagnostics.Debug.WriteLine("UI manifest updated - " + DateTime.Now);

    }, TaskScheduler.FromCurrentSynchronizationContext());
Simon_Weaver
источник
Я предполагаю, что = перед GenerateManifest является опечаткой.
Себастьян Ф.
Да - ушел сейчас! Спасибо.
Simon_Weaver
11

Я просто хотел добавить эту версию, потому что это такая полезная тема, и я думаю, что это очень простая реализация. Я использовал это несколько раз в различных типах многопоточных приложений:

 Task.Factory.StartNew(() =>
      {
        DoLongRunningWork();
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
              { txt.Text = "Complete"; }));
      });
декан
источник
2
Не снижать голосование, так как в некоторых случаях это жизнеспособное решение; однако принятый ответ намного лучше. Он не зависит от технологий ( TaskSchedulerявляется частью BCL, а Dispatcherне является им) и может использоваться для составления сложных цепочек задач из-за того, что не нужно беспокоиться о каких-либо асинхронных операциях запуска и забывания (таких как BeginInvoke).
Кирилл Шленский
@Kirill вы можете немного расширить, потому что некоторые потоки SO единодушно объявили, что диспетчер является правильным методом при использовании WPF WinForms: можно вызывать обновление GUI либо асинхронно (используя BeginInvoke), либо синхронно (Invoke), хотя обычно асинхронно используется потому, что не нужно блокировать фоновый поток только для обновления графического интерфейса. Разве FromCurrentSynchronizationContext не помещает задачу продолжения в очередь сообщений основного потока точно так же, как диспетчер?
Дин
1
Правильно, но OP, безусловно, спрашивает о WPF (и пометил его так), и не хочет сохранять ссылку на какой-либо диспетчер (и я предполагаю, что любой контекст синхронизации тоже - вы можете получить это только из основного потока, и вам нужно хранить ссылку на него где-нибудь). Вот почему мне нравится решение, которое я опубликовал: есть встроенная потоковая статическая ссылка, которая не требует ничего из этого. Я думаю, что это чрезвычайно полезно в контексте WPF.
Дин
3
Просто хочу подкрепить мой последний комментарий: разработчик должен не только хранить контекст синхронизации, но он / она должен знать, что это доступно только из основного потока; эта проблема была причиной путаницы в десятках вопросов SO: люди все время пытаются получить это из рабочего потока. Если их код сам был перемещен в рабочий поток, он завершается ошибкой из-за этой проблемы. Так что из-за распространенности WPF, это, безусловно, следует уточнить здесь, в этом популярном вопросе.
Дин
1
... тем не менее, замечание Дина о том, что [принятый ответ] должен отслеживать контекст синхронизации, если код может отсутствовать в основном потоке, важно отметить, и избегание этого является преимуществом этого ответа.
ToolmakerSteve
1

Получил здесь через Google, потому что я искал хороший способ сделать вещи в потоке пользовательского интерфейса после того, как был внутри вызова Task.Run - Используя следующий код, который вы можете использовать await чтобы снова вернуться в пользовательского интерфейса.

Я надеюсь, что это поможет кому-то.

public static class UI
{
    public static DispatcherAwaiter Thread => new DispatcherAwaiter();
}

public struct DispatcherAwaiter : INotifyCompletion
{
    public bool IsCompleted => Application.Current.Dispatcher.CheckAccess();

    public void OnCompleted(Action continuation) => Application.Current.Dispatcher.Invoke(continuation);

    public void GetResult() { }

    public DispatcherAwaiter GetAwaiter()
    {
        return this;
    }
}

Использование:

... code which is executed on the background thread...
await UI.Thread;
... code which will be run in the application dispatcher (ui thread) ...
дублированный
источник
1
Очень умный! Совершенно не интуитивно, хотя. Я предлагаю сделать staticкласс UI.
Теодор Зулиас