Как стать автором
Обновить

Async/Await в C#. Часть 5. Функция-перечисление и цикл через рекурсию, асинхронный вызов без Async/Await

Уровень сложностиСредний
Время на прочтение8 мин
Количество просмотров6.4K
Всего голосов 6: ↑4 и ↓2+2
Комментарии19

Комментарии 19

Сделать вызов можно просто не написав ключевое слово await. Без await не нужен async.

В чём суть\смысл?

ПС: у вас есть Wait в коде и есть ContinueWith без ожидания. Вы бросаете во все стороны задачи и оно как-то работает. Выглядит как Fire-and-forget, почти никогда в приложении не надо так делать.

Чтобы потом было веселее было разбираться с контекстом синхронизации :)

Вот написал асинхронный UI, но форма блочится. Что не так?

код
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private Task asyncTask1()
        {
            int n1 = 0;
            while (n1 < 10000000)
            {
                n1++;
                textBox1.Text = n1.ToString();
                textBox1.Update();
            }
            return Task.CompletedTask;
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            await asyncTask1();
        }
    }

перегрузка очереди сообщений единственного UI-ного треда.

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

это не для async/await компиляция, тут надо понимать во что вот это

                textBox1.Text = n1.ToString();    
                textBox1.Update();

компилируется.

Нет у вас асинхронности. Просто использование async-await не делает никакой магии.

Вы синхронно крутите while, вот форма и висит.

точно!

Из button1_Click мы вышли для выполнения asyncTask1() (даже если), но сразу зашли в asyncTask1()и в ней повисли

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

Как крутить while отдельно от основной задачи? While тут для имитации действий, где нельзя срезать углы, т.е. все операции и вычисления должны быть произведены полностью и без блокировки формы.

Не обязательно на отдельном вычислительном ядре, ну хоть в отдельном таймслайсе.

Запустите отдельную таску (через Task.Run например).

Чтобы из неё обновлять UI - захватите Dispatcher.CurrentDispatcher из формы в Task-у и передавайте туда изменения. Что-то типа такого должно работать - https://stackoverflow.com/a/16007664

Спасибо за подсказку, вот рабочий вариант.

Hidden text
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private Task asyncTask1()
        {
            int n1 = 0;
            while (n1 < 1000000000)
            {
                n1++;
                Invoke(new Action(() => { textBox1.Text = n1.ToString(); }));
                Thread.Sleep(1000); // чтобы не спамил в UI
            }
            return Task.CompletedTask;
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            await Task.Run(() => asyncTask1());
        }
    }

textBox1.Update() не нужен, UI-поток сам обновляет контролы.

Но было такое ощущение, что async/await уже научили задействовать таски под капотом...

Но было такое ощущение, что async/await уже научили задействовать таски под капотом...

Они немного про разное.

Таска - это отдельный объем работы, обычно выполняемый на тред-пуле (т.е. выполняемый параллельно некоему "основному" треду, в котором создается таска).

async-await - это возможность переиспользовать тред, пока некий запущенный "объем работы" ждёт ответа от внешних источников (сеть, диск). Если сети или диска нет, то async-await в целом не нужен.

И в примерах тоже ждут ответа от внешнего ресурса. Но в чем принципиальное отличие между ожиданием ответа сервера и выполнением вычислений? В обоих случаях крутится некоторый объем кода.

А это в целом тема любой статьи по async-await.

Как работало раньше - был вызов сети или диска, который блокировал поток. При этом в реальности операционная система не делала ничего на ЦПУ (в приложении) и поток с точки зрения приложения был занят, но не использовался.

Как оптимизировали сценарий с async-await. Когда вызов доходит до операционной системы, поток c# приложения возвращается в тред пул (если он из тред пула) и любой другой выполняемый Task может на свободном потоке поработать. Когда ОС сигналит что данные получены (с диска, из сети) - с тред пула берётся просто свободный поток и в нём продолжается выполнение.

Есть особенности, когда что-то идёт по другому, но поэтому я и выше указал, что это тема целых статей. Много мелких особенностей.

Ну тогда я еще спрошу.

await Task.Run(() => asyncTask1());
await Task.Run(new Action(() => asyncTask1()));

Оба варианта вызова дают один и тот же результат, а принципиальная разница в вызове есть? В этих двух случаях анонимная функция без параметров () =>{} принимает тип Func<Task> и Action. А без лямбды можно запустить таску с функцией asyncTask1()? Или без оборачивания в лямбду не обойтись?

насколько я понимаю это одно и тоже, но можно еще проще написать:

await Task.Run(asyncTask1);

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

Лямбды нужны там где нет делегатов.

Если метод не имеет параметров, то можно просто обернуть его в Task.Run:

Task.Run(Console.WriteLine);

даже если вы честно создадите отдельный поток для вашей функции с while-ом, все равно ничего хорошего не получится, как раз из-за того что я сначала написал:

перегрузка очереди сообщений единственного UI-ного треда.

нельзя без ограничений (непрерывно) генерировать Апдейты на UI, то есть написать-то можно, только ничего хорошего из этого не получится, вы ответьте себе на вопрос с какой максимальной частотой у вас должна обновляться картинка на экране, то есть сколько раз в секунду. У вас тут нет ограничения, поэтому вы упираетесь в физические пределы параметров работы оборудования (видеокарты- монитора).

Я отказался от сторонних апдейтов на UI, т.к. если UI не блокируется, то он спокойно обновляет своё состояние сам. Достаточно обновлять данные в контролах.

нашел время внимательно прочитать то что вы написали,

насколько я понимаю в этом вашем примере инкремент:

n1++;

симулирует некоторую, достаточно легковесную вычислительную работу, которую вы хотели бы выполнять непосредственно в UI потоке (при условии что она легковесная это можно делать).

Если это так то скорее всего вам надо бы объявить и вашу функцию:

private Task asyncTask1()

аинхронной, сделать await этой вашей операции с работой, но чтобы await работал - обеспечивал выход из функции (из скомпилированной стайт-машины) надо добавить например

Thread.Sleep(0); в конец этой вашей работы, как-то так:

        private async Task asyncTask1()
        {
            int n1 = 0;
            while (n1 < 10000000)
            {
                n1++;
                textBox1.Text = n1.ToString();
                textBox1.Update();
                await Task.Delay(1);
            }
            return Task.CompletedTask;
        }

я не уверен что все до конца корректно написал, нет возможности скомпилировать - проверить, но надеюсь идея понятна:

идея в том чтобы заставить инкремент и изменение текстового поля выполняться по очереди внутри единственного потока.

Вроде как инкремент выполнится и пошлет изменение контролу,

await прервет итерацию стейт машины,

UI поток выполнит апдейт,

UI поток продолжит выполнение функции asyncTask1()

и так по кругу.

Вообще говоря

await Thread.Sleep(0);

можно воткнуть в любое место в цикле, мне кажется.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории