Продовження завдання на потоці інтерфейсу користувача


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());

Це підходить лише в тому випадку, якщо поточний контекст виконання знаходиться в потоці інтерфейсу користувача.


39
Він дійсний, лише якщо поточний контекст виконання знаходиться в потоці інтерфейсу користувача. Якщо ви покладете цей код в інше завдання, ви отримаєте InvalidOperationException (дивіться розділ Винятки )
stukselbax

3
У .NET 4.5 відповідь Йохана Ларссона слід використовувати як стандартний спосіб продовження завдання в потоці інтерфейсу користувача. Просто напишіть: чекайте Task.Run (DoLongRunningWork); this.TextBlock1.Text = "Завершено"; Дивіться також: blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
Marcel W

1
Thx для врятування мого життя. Я витрачаю години, щоб розібратися, як зателефонувати в основні речі в потоці очікування / ContinueWith. Для всіх інших, як використовується Google Firebase SDK for Unity і все ще має ті самі проблеми, це робочий підхід.
ЧАП

2
@MarcelW - awaitце хороший зразок, але тільки якщо ви знаходитесь у asyncконтексті (наприклад, оголошеному методі async). Якщо ні, то все-таки потрібно зробити щось подібне на цю відповідь.
ToolmakerSteve

33

З асинхронізацією ви просто зробите:

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());

Я здогадуюсь = перед тим, як 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: Можна викликати оновлення графічного інтерфейсу або асинхронно (використовуючи BeginInvoke), або синхронно (Invoke), хоча зазвичай це асинхроніка використовується тому, що не хотілося б блокувати фоновий потік лише для оновлення графічного інтерфейсу. Чи не FromCurrentSynchronizationContext не ставить завдання продовження в черзі повідомлень основного потоку так само, як диспетчер?
Дін

1
Правильно, але ОП, безумовно, питає про WPF (і позначив це так), і не хоче зберігати посилання на будь-якого диспетчера (і я припускаю, що будь-який контекст синхронізації також - ви можете отримати це лише з головної нитки, і вам потрібно десь зберігати посилання на нього). Ось чому мені подобається рішення, яке я опублікував: вбудована статична посилання, захищена від потоку, яка нічого цього не вимагає. Я думаю, що це надзвичайно корисно в контексті WPF.
Дін

3
Просто хотів підкріпити мій останній коментар: Розробник повинен не тільки зберігати контекст синхронізації, але він / вона повинен знати, що це доступно лише з головної нитки; ця проблема стала причиною плутанини в десятках питань щодо ПС: Люди весь час намагаються отримати це від робочої нитки. Якщо їх код сам переміщений у робочу нитку, він не працює через цю проблему. Отже, через поширеність 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) ...

Дуже розумний! Досить неінтуїтивно, хоча. Я пропоную зробити staticклас UI.
Теодор Зуліяс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.