Ожидание задачи в потоке пользовательского интерфейса без взаимоблокировки - PullRequest
0 голосов
/ 18 июня 2019

Имейте в виду: я нахожусь на .NET 4.0, и не может использовать шаблон асинхронного / ожидания , ни .ConfigureAwait.

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

Поэтому я использовал Task.Factory.StartNew, чтобы запустить новое задание в потоке пользовательского интерфейса, и Wait, чтобы дождаться его завершения.

Важно, чтобы операция завершилась до того, как я продолжу, поэтому я использовал Wait, чтобы дождаться ее завершения. Однако это создает тупик в потоке пользовательского интерфейса.

Полный код:

// currently on the UI thread
Task.Factory.StartNew(() => LongerOperation())
.ContinueWith(x =>
{
    // simple output that I'm done
}).Wait(); // -> deadlock. can't remove it, otherwise the app would continue

Вызов этого кода выглядит как обычный вызов функции

private void Run(){
    DoStuff();
    DoMoreStuff(); // it's important that DoStuff has finished, that's why removing .Wait won't work
}

private void DoStuff()
{
    Task.Factory.StartNew(() => LongerOperation())
    .ContinueWith(x =>
    {
        // simple output that I'm done
    }).Wait();
}

Как я могу дождаться завершения задачи, не создавая тупик в потоке пользовательского интерфейса? Другие ответы предлагают использовать шаблон async / await, но я не могу его использовать.

Ответы [ 2 ]

0 голосов
/ 24 июня 2019

Попробуйте MSDN: Backgroundworker .Вы можете отменить свою задачу, выполнив backgroundworkerObject.CancelAsync();, если вы выполнили backgroundWorkerObject.WorkerSupportsCancellation = true; в конструкторе формы.Это выполнит вашу долгую операцию в отдельном потоке, не вызывая тупиковую ситуацию в вашем пользовательском интерфейсе.

0 голосов
/ 18 июня 2019

Для начала вы, вероятно, не работаете в .NET 4.0, потому что среды выполнения .NET 4.x являются двоичными заменами.Установка приложения .NET 4.5+ или исправления Windows Update означает, что все приложения теперь работают на .NET 4.5 и более поздних версиях.Ваша машина разработки работает по крайней мере на .NET 4.5+, что означает, что вы уже разрабатываете * отличную среду выполнения, чем та, на которую вы собираетесь ориентироваться.

Единственное исключение, если вы нацелены на неподдерживаемую ОСверсии, такие как Windows XP и Windows Server 2003, которые никогда не получали поддержку .NET 4.5.

В любом случае, использование Microsoft.Bcl.Async является жизнеспособным вариантом,поскольку вы уже приняли на себя гораздо большие риски, например, работаете на 4.5 и нацелены на 4.0 или работаете на неподдерживаемой ОС, у которой даже нет поддержки TLS1.2, минимальное требование для большинства сервисов в настоящее время, включая Google, AWS, Azure,банки, платежные шлюзы, авиакомпании и т. д.

Другой вариант заключается в использовании ContinueWith с параметром TaskScheduler, который указывает, где запустить продолжение. TaskScheduler.FromCurrentSynchronizationContext () указывает, что продолжение будет выполняться в исходном контексте синхронизации.В приложении Winforms или WPF это поток пользовательского интерфейса.

Тогда мы писали такой код:

Task.Factory.StartNew(() => LongerOperation())
.ContinueWith(t =>
{
    textBox.Text=String.Format("The result is {0}",t.Result);
},TaskScheduler.FromCurrentSynchronizationContext());

Обработка исключений и объединение в цепочку нескольких асинхронных операций требует дополнительной работы.t.Result выдаст исключение, если задача не выполнена, поэтому вам нужно проверить Task.IsFapted или Task.IsCancelled , прежде чем пытаться использовать его значение.

Невозможно замкнуть цепь продолжений с помощью простого return, как вы можете в .NET 4.5 и async / await.Вы можете проверить флаг IsFaulted и избежать обновления пользовательского интерфейса, но вы не можете предотвратить выполнение продолжения next вниз по цепочке.

Task.Factory.StartNew(() => LongerOperation())
    .ContinueWith(t=>
    {
        if (!t.IsFaulted)
        {
            return anotherLongOperation(t.Result);
        }
        else
        {
            //?? What do we do here?
            //Pass the buck further down.
            return Task.FromException(t.Exception);
        }
    })         
    .ContinueWith(t =>
    {
        //We probably need `Unwrap()` here. Can't remember
        var result=t.Unwrap().Result;
        textBox.Text=String.Format("The result is {0}",result);
    },TaskScheduler.FromCurrentSynchronizationContext());

Чтобы остановить выполнение в случаев случае неудачи вам придется использовать параметр TaskContinuationOptions и передать, например, NotOnFaulted.

Task.Factory.StartNew(() => LongerOperation())
.ContinueWith(t =>
    {
        textBox.Text=String.Format("The result is {0}",t.Result);
    },
    CancellationToken.None,
    TaskContinuationOptions.NotOnFaulted,
    TaskScheduler.FromCurrentSynchronizationContext());

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

В конце, использование Microsoft.Bcl.Async может привести к гораздо более простому коду

Task.Factory.StartNew(() => LongerOperation())
.ContinueWith(t=>
{
    if (!t.IsFaulted)
    {
        var num=anotherLongOperation(t.Result);
        if (num<0)
        {
            //Now what?
            //Let's return a "magic" value
            return Task.FromResult(null);
        }
    }
    else
    {
        //?? What do we do here?
        //Pass the buck further down.
        return Task.FromException(t.Exception);
    }
})         
.ContinueWith(t =>
{
    //This may need t.Result.Result
    if (!t.IsFaulted && t.Result!=null)
    {
        textBox.Text=String.Format("The result is {0}",t.Result);
    }
},TaskScheduler.FromCurrentSynchronizationContext());

Я не уверен, нужны ли продолжения внизу Unwrap() или нет.Они, вероятно, делают, поскольку второй шаг возвращает Task<T>.Забыв об этом, вы можете столкнуться с трудностями в отладке.

Использование Microsoft.Bcl.Async приводит к более чистому и простому коду, который намного проще понять:

public async Task DoRun()
{
    try
    {
        var x=await longOperation();
        var num=await anotherLongOperation(x);
        if(num<0)
        {
            return;
        }
        textBox.Text=String.Format("The result is {0}",num);
    }
    catch(Exception exc)
   {
      //Do something about it
   }
}

Обновление

Как отметил Стивен Клири, BLC Async работал только с Visual Studio 2012. Он также больше не поддерживается и, вероятно, доступен только для загрузки для подписчиков MSDN.

Еще в 2010 году группа разработчиков Parallel выпустила несколько расширений и примеров для упрощения параллельной и асинхронной обработки, Дополнительные возможности параллельных расширений .Эта библиотека включала в себя такие расширения, как Then, которые значительно упростили цепочку и обработку ошибок.

Большинство этих функций включены в саму .NET начиная с 4.5, поэтому образцы и библиотека не обновлялись с 2011 года. Любой NuGetпакеты или репозитории Github, которые претендуют на роль Parallel Extras, просто клонируют и перепаковывают эту загрузку.

Эта библиотека использовала для документирования серии статей Стивена Туба но Microsoft недавно изменила свой механизм ведения блогов, и URL-адреса старых перенесенных сообщений в блоге больше не работают.

Поскольку ни .NET 4.0, ни Parallel Extras не поддерживаются, ссылки не были исправлены.Эти статьи по-прежнему доступны под другим URL-адресом

«Больше не поддерживается» означает, что документы, библиотеки и знания также могут быть потеряны.Некоторые из тех, кто работал с заданиями 7 лет назад, могут помнить библиотеки и проекты, которые использовались тогда, но 7 лет - это слишком долго.

...