Непонятно с async-await в C# - 2
Удалите эту тему, пожалуйста Непонятно с async-await в C# - Программирование (germany.ru)
Вот задачка - что выведет код? Можно без временных меток (их для удобства просто добавил) - просто в какой последовательности сообщения будут?
class Program { static DateTime _startTime; static async Task Main(string[] args) { _startTime = DateTime.Now; var wait1 = Wait1(); var wait2 = Wait2(); var wait3 = Wait3(); await wait1; Console.WriteLine("after wait1"); await wait2; Console.WriteLine("after wait2"); await wait3; Console.WriteLine("after wait3"); } static async Task Wait1() { await Task.Delay(3000); Console.WriteLine($"wait1 is ready {(DateTime.Now - _startTime).Seconds}"); } static async Task Wait2() { await Task.Delay(2000); Console.WriteLine($"wait2 is ready {(DateTime.Now - _startTime).Seconds}"); } static async Task Wait3() { await Task.Delay(1000); Console.WriteLine($"wait3 is ready {(DateTime.Now - _startTime).Seconds}"); } }
У меня выводит
wait3 is ready 1
wait2 is ready 2
wait1 is ready 3
after wait1
after wait2
after wait3
и я не понимаю, почему. Я думал, что сообщения должны выводиться в той же последовательности, в которой заканчиваются выполняться таски. Т.е.
wait3 is ready 1
after wait3
wait2 is ready 2
after wait2
wait1 is ready 3
after wait1
Варинаты с разным временем ожидания
--------------------
1000
3000
2000
wait1 is ready 1
after wait1
wait3 is ready 2
wait2 is ready 3
after wait2
after wait3
------------------
3000
1000
2000
wait2 is ready 1
wait3 is ready 2
wait1 is ready 3
after wait1
after wait2
after wait3
-------------------
2000
3000
1000
wait3 is ready 1
wait1 is ready 2
after wait1
wait2 is ready 3
after wait2
after wait3
---------------
2000
1000
3000
wait2 is ready 1
wait1 is ready 2
after wait1
after wait2
wait3 is ready 3
after wait3
Когда бы ни закончились ожидаемые таски, код между ними всегда выполняется в том порядке, как написан
after wait1
after wait2
after wait3
Причём первая строка выполняется лишь после того, как закончится таска, ожидаемая перед этой первой строкой, независимо от того, закончились ли последующие таски.
Я тут вообще логики не вижу.
По сути, писать асинхронный код с ожиданием задач в таком виде мало смысла. Если я хочу, чтобы после каждой таски выполнялся свой код, и именно тогда, когда эта таска закончится, то нельзя писать этот код сразу после таски. Иначе этот код выполнится лишь тогда, когда все остальные таски и код выше выполнится.
Т. е. либо надо обязательно использовать ContinueWith, либо WhenAll, WhenAny и т.п. вспомогательные методы. Тот поток выполнения, что в вышеприведённом примере - нихрена не логичен.
https://docs.microsoft.com/en-us/dotnet/csharp/language-re...
The await
operator suspends evaluation of the enclosing async method until the asynchronous operation represented by its operand completes. When the asynchronous operation completes, the await
operator returns the result of the operation, if any. When the await
operator is applied to the operand that represents an already completed operation, it returns the result of the operation immediately without suspension of the enclosing method. The await
operator doesn't block the thread that evaluates the async method. When the await
operator suspends the enclosing async method, the control returns to the caller of the method.
Я тут вообще логики не вижу.
Не нужно просто вызывать асинхронные методы синхронно.
Поиграйтесь лучше с этим кодом.
class Program { private static DateTime _startTime; static async Task Main(string[] args) { _startTime = DateTime.Now; await Wait1(); Console.WriteLine("after wait1"); await Wait2(); Console.WriteLine("after wait2"); await Wait3(); Console.WriteLine("after wait3"); } private static async Task Wait1() { await Task.Delay(3000); Console.WriteLine($"wait1 is ready {(DateTime.Now - _startTime).Seconds}"); } private static async Task Wait2() { await Task.Delay(2000); Console.WriteLine($"wait2 is ready {(DateTime.Now - _startTime).Seconds}"); } private static async Task Wait3() { await Task.Delay(1000); Console.WriteLine($"wait3 is ready {(DateTime.Now - _startTime).Seconds}"); } }
код, который должен выполниться немедленно после завершения этой Task
То есть, независимо от других задач? -> https://docs.microsoft.com/en-us/dotnet/api/system.threadi...
код, который должен выполниться немедленно после завершения этой TaskТо есть, независимо от других задач? -> https://docs.microsoft.com/en-us/dotnet/api/system.threadi...
Да-да, я так и подумал - нужно использовать вспомогательные методы.
Не нужно просто вызывать асинхронные методы синхронно.
Поиграйтесь лучше с этим кодом.
Ну тут вообще всё последовательно. У меня хотя бы таски запускаются параллельно, но ожидаются последовательно - т.е. общее время выполнения всех тасков не больше времени для самой долгой таски. А у вас всё запусается и ожидается последовательно, поэтому общее время выполнения всех тасков всегда суммируется.
А какая стоит задача то?
С этим кодом может быть примерно такое извращение для паралельного исполнения
private static void RunParallel() { Task[] tasks = new Task[3]; tasks[0] = Task.Run(async () => { await Wait1(); }); tasks[1] = Task.Run(async () => { await Wait1(); }); tasks[2] = Task.Run(async () => { await Wait1(); }); Task.WaitAll(tasks); }
А так хотя бы сюды нужно глянуть
https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/
А какая стоит задача то?
А так хотя бы сюды нужно глянутьhttps://devblogs.microsoft.com/pfxteam/await-synchronizati...
Я даже эту штуку пару раз проштудировал. А толку? Если не юзаешь постоянно, то вылетает из головы.
А так я играюсь с кодом. Я к этому async await уже раза три подходил, и каждый раз какой-то затык. Там куча подходов и паттернов применяется, и их местами смешивают. Даётся много свободы, но при этом правильных подходов не так много. Зато есть огромное число возможностей сделать неправильно. В этом и сложность. В этом отличие от коллбэчного подхода, где делается только вот так и никак иначе - не ошибёшься.
В МСДН написано, что "асинхронный" подход сделан, чтобы упростить многопоточное программирование по сравнению с "коллбэчным", но по факту они только больше всё запутали - без большого опыта и знания всех подводных камней легко выстрелить себе в ногу.
Вообще, у меня ещё отсюда тянется c# - How to close modal window after a Task completes? - Stack Overflow
Вроде, решения подходящие мне привели, но я так и не получил понимания, как самому до этого дойти. Тасуя туда-сюда метод закрытия окна, и не понимаю, почему он не срабатывает в определённых случаях, хотя вроде должен.
Вот из этого примера видно, что данный способ отображения прогресса и автозакрытия окна можно написать небольшим числом правильных способов и огромным числом неправильных. И нет одного шаблона, как в коллбэчном подходе, где указал методы для самой задачи, для задачи по завершению операции, и для задачи при возникновении ошибки - и так всегда однообразно поступаешь.
По первому видео возник вопрос. Вот картинка оттуда
Некий внешний код вызывает эту функцию, выполнение функции продолжается до await, если ожидаемая при await операция не завершена, выполнение возвращается в код, вызвавший показанную функцию. Далее выполнение продолжается во внешнем коде, но когда операция при await завершается, выполнение во внешнем коде блокируется и продолжается в данной функции после await?
Если да, то получается, что каждое завершение ожидания задачи в вызванной асинхронной функции прерывает вызвавший эту функцию код? Напишу много await - вызывающий код будет столько же раз прерван (если ожидаемые задачи не выполнены к коменту ожидания)?
А, понял. Если мы вызвали метод ConvertAsync, а он запустил другие задачи и их ожидает, то выполнение возвращается в вызвавший ConvertAsync код после первого же await в ConvertAsync. А вызвавший код, в свою очередь, либо ждёт, пока весь метод ConvertAsync не выполнится, либо просто забывает про этот метод (запустил и забыл). Соответственно, даже если после первого await внутри ConvertAsync выполнение в нём продолжается, вызвавший ConvertAsync код всё ещё либо ждёт, либо уже забыл.
Т.е. внутри ConvertAsync может быть сколько угодно пауз на ожидание выполнения других задач, но вызвавший ConvertAsync код либо ждёт до конца метода ConvertAsync, либо лишь запускает его и ничего от него не ждёт.
Вобщем, с таской вызывающий код может сделать в основном 2 вещи - либо запустить её, либо ожидать, либо запустить и ожидать. Каких-то промежуточных вариантов, типа запустить и ожидать частичного выполнения (до какого-нибудь промежуточного await внутри этой таски) нельзя. Соответственно, либо вызывающий код ждёт всю таску до конца, либо "забывает" про неё.
Ещё можно запустить её как синхронную, с блокировкой текущего контекста вызывающего кода, но это к таскам не относится - это обычный синхронный код.
Понять как эта штука работает можно только, если понять как она реализована через event loop. Таски это то, что добавляется в event loop и там их куча накаплинвается. Выполняются они одним потоком, который в явно заданные моменты может перескакивать на другики таски. Вот и все. Одинаково работает в ЖС, Питоне и др. языках. А из официальных доков и примеров ничего не понять. Просто надо понять когда таски добавляются, когда они подвешиваются, когда удаляются. И усе.
Одна event loop полезна, но весьма ограниченно. Иногда надо несколько event loops (каждая со своим потоком), либо просто еще workers с очередью. Вот тогда уже весело будет.
Если юзеру (программисту) дают технологию, с которой нельзя разобраться без того, чтобы досконально не изучать её нутро, то это плохая технология. Это абсолютно дырявые абстракции.
Или, как вариант, программисты слишком глубоко копают, что многим из них не нужно. Как я понял, с async/await важно знать только, что это необязательно выполняется в другом потоке (т.е. это не многопоток в чистом виде), и чего с ними можно делать, а чего нельзя. И несколько use cases. Лезть внутрь - вот зачем? Ну выкитал там в видео человек портянку какого-то странного кода. Я что, чтобы работать с async/await, должен эту и другие подобные портянки всегда в голове держать, и всегда прогонять свой код использования async/await через эти портянки - а что там внутри мол произойдёт? Проще иметь список абстрактных правил, если эти абстракции не сильно протекают. Ну это типа как просто водить машину, и уметь подшаманить там что-нибудь самому. Оно конечно полезно - уметь самому, но нахрена оно мне надо, если я программист, а нифика не автомеханик, ни слесарь, и у меня даже хобби такого нет - торчать под машиной? Вон джаваскриптеры вообще не знают, когда у них какого типа данные, и что получается при разных манипуляциях с ними - и ничё, нормально живут, бабки зарабатывают не меньше всяких шарперов, а работы и вакансий у них на порядок больше, чем у этих шарперов.
Возможно, просто раньше были хреново написаны тьюториалы. Да и сейчас, как я понял, вся эта инфа раскидана по нескольких разделам, а не собрана в одном месте.
Но в любом случае, async/await не заменяет нормальную работу с потоками (Thread, lock, всякие MemoryBarrier и прочее). Даже, пожалуй, наоборот - если ты хорошо знаешь, как работать с потоками и писать потокобезопасный код, ты можешь вообще без async/await обходиться, а просто загонять всё явно в другие потоки и "вручную" синхронизировать их.
Я в своей программе делал проще. У меня в реальном времени с платы сбора данных поступали данные. Их надо было обсчитывать, надо было результаты обсчётов в реальном времени выводить на графики и обновлять показания в интерфейсе. Я сделал что-то вроде конвейера. Приходящие данные с платы записывались в массив в одном потоке - этот поток постоянно опрашивал порт и считывал из него новые данные. Другой поток брал сохранённые в предыдущий массив данные, делал первоначальную обработку и сохранял в другой массив. Тут не нужна была синхронизация и блокировки, т.к. один поток только писал в массив, а другой только читал из него же, отслеживая новые данные по размеру массива и последнего прочитанному индексу, и записывал результаты в следующий массив. Ну и так далее - было что-то около 3-4 таких "уровней". Ещё один поток брал данные из последнего массива, делал окончательный расчёт и сохранял в специальные структуры результатов - тоже в виде массива. Следующий поток данные из этих структур брал и обновлял интерфейс. И все эти потоки молотили постоянно - на каждом уровне обработки данных я опрашивал размер массива из предыдущего уровня - не увеличился ли он достаточно, чтобы набрать очередной пакет данных для расчёта. И я эти потоки разве что притормаживал через тупой Thread.Sleep(), т.к. постоянный опрос массива в бесконечном цикле естественно займёт ядро процессора на 100%.
Тогда я столкнулся с плохой производительностью графической системы из-за его так называемой retained model (я достаточно глубоко копаю?) и странной логикой отрисовок.
Там ещё ссылки есть на интересные статьи и видео
Introducing Direct2D | Microsoft Docs (искать по слову "retain")
Michael Wallent: Advent and Evolution of WPF | Charles | Channel 9 (msdn.com)
Конечно, это было костыльно. Но я тогда первый раз с многопоточностью работал, а надо было сделать не чтобы "на века" и "другие люди же будут пользоваться", а чтобы запустилось ещё вчера, т.к. завтра ехать на испытания в соседнее фермерское хозяйство. Я постепенно кое-где переписывал на async/await, где создание потоков было лишним, с блокировками (lock) повозился. Надо было вообще не опрашивать постоянно массивы, а что-то вроде событий организовать - мол, новых данных в массиве достаточно, чтобы набрался пакет для кванта расчёта (нужно было некое минимальное количество данных). Кое-где я и сделал что-то подобное. Но так до конца и не довёл - уже уезжал.
Я что, чтобы работать с async/await, должен эту и другие подобные портянки всегда в голове держать, и всегда прогонять свой код использования async/await через эти портянки - а что там внутри мол произойдёт?
------
А чему тебя в "школе" учили?
Меня, например, учили, что запоминать много и быстро - это очень хорошо, но встречается крайне редко.
А чтобы использовать то, что требуется "запоминать много и быстро", надо не запоминать, а строить небольшую более менее понятную модель и использовать ее как ключ чтобы вспоминать то, что нужно... Для меня персонально - предпочтительно графическое представление, но знаю людей которым достаточно и мнемонических вариантов...
если ты хорошо знаешь, как работать с потоками и писать потокобезопасный код, ты можешь вообще без async/await обходиться
-----
Разумеется. Бо, писали все что нужно когда не было async/await и в помине.
Есть только одна проблема - чтобы сделать все грамотно на потоках нужно уметь пользоваться потоками и... написать где-то в 15 раз больше кода...
Тут не нужна была синхронизация и блокировки, т.к. один поток только писал в массив, а другой только читал из него же
-----
Об а на...
Слыхал, федотыч, один - пишет, другой - читает... и ни синхронизации, ни блокировки не надо-ть... во как...
один - пишет, другой - читает... и ни синхронизации, ни блокировки не надо-ть... во как...
Я имел ввиду, что один поток постоянно добавляет данные в массив - не изменяет сами данные. А друго поток только считывает новые данные. Их действия никак не пересекаются.
По аналогии, разгружают фуру одни грузчики и раскладывают груз в линию. А другие грузчики подходят и выложенный груз уносят на склад. По вашей логике, если с одного конца линии грузчики добавляют груз с фуры, то с другого конца другие грузчики брать этот груз не могут, и наоборот - надо заблокировать и синхронизировать.
Если организовать доступ к данным, чтобы деятельность потоков не пересекалась, то можно ничего не блокировать и не синхронизировать (синхронизировать нечего). Это как конвейер - пока одни
делают что-то в одной части конвейера, другие делают другое в другой части - никто доступ другим к конвейеру не блокирует. Заблокирован только доступ к другим частям конвейера, за которые данные работники не отвечают и им они не нужны. Деятельность работников на конвейере не пересекается.
Там, где у меня что-то пересекалось, там я блокировал.
Я имел ввиду, что один поток постоянно добавляет данные в массив - не изменяет сами данные. А друго поток только считывает новые данные. Их действия никак не пересекаются.
Очевидно, что они пересекаются как минимум на уровне массива. И именно там надо синхронизировать ;) Не зря же сделали Concurrent-контейнеры ;)
Но даже с твоей аналогией все не так очевидно ;)
По аналогии, разгружают фуру одни грузчики и раскладывают груз в линию. А другие грузчики подходят и выложенный груз уносят на склад. По вашей логике, если с одного конца линии грузчики добавляют груз с фуры, то с другого конца другие грузчики брать этот груз не могут, и наоборот - надо заблокировать и синхронизировать.
Дело в том, в этом примере лента является синхронизируещем механизмом. Предлагаю убрать у ленты механику, т.е. вместо ленты у нас есть просто Х ячеек. При этом разгружающий фуру рабочий (а он должен быть один) должен класть коробки как можно ближе к себе, а рабочий склада (тоже один) тоже должен брать ближайшую к себе коробку. Получается такой FIFO контейнер. Так вот очевидно, что в момент, когда разгружающий фуру рабочий будет класть очередную коробку в ячейку, рабочий склада должен стоять и ждать ;)
В своей аналогии несколько грузчиков разгружают фуру и заполняют ленту и несколько рабочих уносят коробки на склад. В такой конструкции надо еще и блокировать доступ к ленте, так что без синхронизации никуда ;)
Вобщем, сейчас на всё есть готовые буквари, типа такого
How to: Implement a producer-consumer dataflow pattern | Microsoft Docs
А раньше я городил велосипеды.
Дело в том, в этом примере лента является синхронизируещем механизмом. Предлагаю убрать у ленты механику, т.е. вместо ленты у нас есть просто Х ячеек. При этом разгружающий фуру рабочий (а он должен быть один) должен класть коробки как можно ближе к себе, а рабочий склада (тоже один) тоже должен брать ближайшую к себе коробку. Получается такой FIFO контейнер. Так вот очевидно, что в момент, когда разгружающий фуру рабочий будет класть очередную коробку в ячейку, рабочий склада должен стоять и ждать ;)
В своей аналогии несколько грузчиков разгружают фуру и заполняют ленту и несколько рабочих уносят коробки на склад. В такой конструкции надо еще и блокировать доступ к ленте, так что без синхронизации никуда ;)
Вы под синхронизацией понимаете некое место с объектом синхронизации, типа lock(synchronisationObject)?
Вобщем, к чёрту аналогии. Конкретно что было - был массив, в который писались данные с платы сбора данных в одном потоке. В другом я брал эти данные. Но нужно было брать какое-то минимальное число данных, необходимое для кванта расчёта. Поэтому читающий поток постоянно проверял, не пришло ли достаточно новых данных, затем считывал их, если пришло.
Массив был банальным списком. Про списки написано
List Class (System.Collections.Generic) | Microsoft Docs
It is safe to perform multiple read operations on a List, but issues can occur if the collection is modified while it's being read.
Как я понял, модификация коллекции означает - что сами элементы меняют или переставляют - т.е. удаляют, вставляют в середину и прочее. Если только добавляют в конец, то можно параллельно читать безопасно из той части, что уже была записана.
Если нет, то почему не потокобезопасно? Там внутри могут переаллокации старых данных происходить, если ты просто добавляешь данные в список? Но тогда сложность вставок не была бы О(1)
Collections and Data Structures | Microsoft Docs
Ещё раз - я при чтении не могу выйти за пределы списка, т.к. его размер постоянно проверяется. Изо всех "модификаций" списка - только добавление данных в конец. Всё. Всё равно не потокобезопасно читать? Штука в том, что у меня никогда проблем не было, если обращаться со списками таким образом - т.е. каких-то сбоёв или аномально прочитанных данных.
Что-то типа такого накидал на .NET 5. Одним потоком пишем в лист случайное число данных в определённых пределах, другим читаем каждый раз фиксированное число НОВЫХ данных, если их набралось достаточно, нажимая на "у". Сделал паузу при добавлении данных в 3 секунды специально, чтобы если слишком часто нажимать на "у" ничего не читалось. Правда, клавиатурный воод буферизуется, поэтому все нажатия "у" запоминаются, даже если данных на данный момент не достаточно, и потом автоматом вводятся - следовательно, выводятся и значения из данных, даже если "у" больше не нажималось. Как это отключить - пока не придумал. Да и не требуется особо.
Операция чтения data.Count атомарная, насколько я понимаю, поэтому её чтение всегда даст число данных на текущий момент, как бы в список
данные ни добавлялись. Да это и не важно - Count не может уменьшаться, а значит, я никогда не выйду за пределы реального числа данных в списке, кроме случая переполнения.
В таком предельно упрощённом случае работы потоков со списком блокировки и синхронизации не делаю - они не нужны. Если, конечно, там внутри при добавлении данных и чтении методами AddRange и GetRange не происходит каких-то потокобезопасных переаллокаций, который повлияют на порядок чтения данных из списка - например, поменяют индексы значений в списке. Но, судя по всему, такой дичи там не происходит - нелогично было бы, что при добавлении данных список бы внутри как-то передвигал значения уже хранящихся элементов или двигал их в памяти так, что при чтении клиент попадал бы на другие элементы. Ну и судя по работе программы, таких кульбитов не происходит. Может, они происходят очень редко?
Просто, на мой взгляд, в МСДНе дана общая рекомендация - список и его публичные методы не потокобезопасны в общем случае. А я предельно упростил, так что хоть список и не потокобезопасен, но по моей логике работы с его данныеми с ними ничего произойти не может.
using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp1 { class Program { static List<int> data = new(); static int dataReadSize = 5; static int currentIndexToRead = 0; static Random r = new(); static bool doWork = true; static void Main(string[] args) { Thread writeThread = new(new ThreadStart(WriteData)); writeThread.IsBackground = true; writeThread.Start(); Thread readThread = new(new ThreadStart(ReadData)); readThread.IsBackground = true; readThread.Start(); // to prevent app to end immediately while (doWork) Thread.Sleep(100); } static void WriteData() { while (doWork) { data.AddRange( Enumerable.Range( 0, r.Next(3, 9))); Thread.Sleep(3000); // imitate pause } } static void ReadData() { Console.WriteLine("Press 'y' to continue or any another key to stop."); while (doWork) { int howMuchCanRead = data.Count - currentIndexToRead; bool canRead = howMuchCanRead >= dataReadSize; if (canRead) { if (Console.ReadKey(true).Key != ConsoleKey.Y) doWork = false; var buffer = data.GetRange(currentIndexToRead, dataReadSize); currentIndexToRead += dataReadSize; Console.WriteLine(string.Join(',', buffer)); } } } } }
Ну закон сохранения энергии и тут работает.
Т.е. вы согласны с моей идеей, что при таком подходе к списку можно без синхронизаций и блокировок обойтись?
А теперь тоже самое, но с маленьким дополнением: количество элементов массива-буфера не должно превышать N.
Так я и так не читаю больше, чем dataReadSize.
Если из потока, то я считывал так - здесь буфер каждый раз разной длины:
while (_doWork) { Thread.Sleep(30); // to not keep the thread busy all time, and to allow TcpClient accumulate sufficient amount of data if (_client.Available > 0) { _buffer = new byte[_client.Available]; // _client is of type TcpClient _stream.Read(_buffer, 0, _buffer.Length); // _stream is of type NetworkStream RawData.AddRange(_buffer); // RawData is of type List<byte> } }
Очень интересная интерпретация, а ссылочку на теорию мона?
Это не интерпретация - это основы. А интерпретация это как реализовано с C#, Python или других языках или либах. Поэтому я и говорю, что лучше все-таки разобраться в основах, чем как это реализовано в каком-то языке. По крайней мере когда я въезжал в тему, мне так было существенно быстрее.
Вот ссылки: Coroutine, Event_loop
Смысл простой:
- Корутины (например, объявляются через async) нельзя вызвать и получить значение. Единственно что можно это добавить ее как таск в очередь и поучить назад объект, который эту задачу представляет (фьючерс, промис и т.п.)
- Внутри корутины можно ожидать (например, через await) когда другая таска
родитзавершится и получить ее результат. Это явный сигнал диспетчеру.
Что происходит
с тасками в очереди это забота диспетчера и его логики. Но по простому таска либо котова к выполнению либо не готова (ждет результата). Когда поток освобождается он выбирает из тех, кто готов. Если таска заканчиватся, то он помечает как готовые ту таску, которая ждала этого результат.
Зачем это нужно и где полезно это другая тема. Кстат, хоть ЖС заслуженно ругают, у него есть одна большая заслуга - он первым ввел такой механизм в малограмотные массы.
Но в любом случае, async/await не заменяет нормальную работу с потоками (Thread, lock, всякие MemoryBarrier и прочее).
Ну понятно что не заменяет - это разные модели.
Даже, пожалуй, наоборот - если ты хорошо знаешь, как работать с потоками и писать потокобезопасный код, ты можешь вообще без async/await обходиться, а просто загонять всё явно в другие потоки и "вручную" синхронизировать их.
Для async/await (в базовой модели) нужен только один поток, поэтому про синхронизацию и потоки можно спокойно забыть (что в ЖС и происходит). А реализовать самому конечно можно, чем люди десятки лет занимались. Например, на чем поднялся nginx? Правильно, на неблокировочных операциях. Это значит что может быть тысячи активных соединений (таски в очереди) и один рабочий поток со всем справляется, когда у стандартного Апачи есть несколько рабочих потоков и он скрипит от напряга, поскольку потоки блокируются.
У меня в реальном времени с платы сбора данных поступали данные.
Это вообще другая тема. Тут надо посмотреть в сторону стриминга: ReactiveX, например, для C# GitHub - dotnet/reactive: The Reactive Extensions for .NET
Так я и так не читаю больше, чем dataReadSize.Нет, data.Count <=N
Если вы имеет ввиду ограниченный буфер, куда все пришедшие данные складируются, то можно использовать что-то вроде кольцевого буфера - т.е. перезаписываемого с начала, когда буфер заполнен. Тут нужно только успевать считывать, чтобы данные не накапливались. Тогда в код считывания с такого буфера нужно просто добавить логику, перебрасывающую чтение очередного пакета данных с конца на начало - это просто, т.к. размеры буфера и считываемого пакета известны.
Кольцевые буферы много где применяются. Например, на портах, вроде, так устроено - буферы там довольно ограниченные, и они перезаписываются с начала, когда достигнут конца. Ещё я использовал кольцевой буфер на графике, который в реальном времени показывал ход эксперимента. Там и всплыла проблема WPF - окошко графика не можно отрисовать более примерно 10-15к точек в секунду. Т.е. на графике только оси координат и обозначения, которые постоянны и не меняются, и постоянно добавляющиеся данные эксперимента в виде точек, соединённых линиями - больше ничего. Если хочешь, чтобы график обновлялся хотя бы 5-8 раз в секунду, то больше 1-2к точек не показывай. Т.е. накопление данных на графике не подходило никак - пришлось воспользоваться "бегущим" графиком через кольцевой буфер (взял из поставки графика, а не сам написал). Всё равно там пришлось городить систему оптимизации данных, посылаемых на отрисовки, которая брала не все данные, а, скажем, каждое 5, и только его отрисовывало. Ну и не тупо каждое 5, а с сохранинем экстремумов, чтобы не пропускать максимальные и минимальные значения, которые важны оператору, проводящему эксперимент. Вобщем, довольно навороченная и настраеваемая система оптимизации по данным была (для всего - для приёма с устройства, для отрисовок, для расчёта и прочего).
Но это не подходит в случае, когда нужно хранить данные всего эксперимента. Тогда нужно либо сбрасывать на ROM, либо ещё куда, чтобы потом можно было достать (с соответствующим замедлением обработки, конечно).
В моём случае скорость поступления данных была в максимуме где-то около 10000 4-байтовых (int) значений в секунду. Эксперимент длится в пределе минут 5 (это с кратным запасом) - т.е. это 10000 значений * 4 байт * 60 секунд * 5 минут = 12М байт. Из-за особенностей расчёта эти данные с разными перерасчётами дублировались на разных слоях раз 10 - т.е. в сумме 120М байт в RAM. Ну и на всё остальное сколько-то памяти нужно - вобщем, в 400-500М байт я укладывался за 5 минут работы программы. Если не оставлять программу включенной постоянно, то вылетать с переполнением памяти не будет. Правда, при 10к данных в секунду у меня уже тормозил процессор и расчёт. Да и такая точность была не нужна. Там по сути много данных шло из-за того, что нужно было определять обороты в минуту, а для этого использовались метке на вращающемся теле. Меток должно было быть в пределах 50-400 (в идеале - чтобы было видно профиль работы каждого цилиндра двигателя в одном обороте, если двигатель многоцилиндровый). На реальных двигательных установках скорость вращения не превышала 10к оборотов в минуту (166 в секунду). Т.е. теоретический максимум был 33к метки (т.е. 4-байтовых данных) в секунду, но это прям вообще теоретический. Реалистичный максимум не превышал 4-5к в секунду. Да и задачи, чтобы подходило подо все возможные обороты в мире не ставилось. Т.е. всякие "формулы 1" с их 15-19 rpm и ракетные топливные турбины с их, доходящими до 100к rpm не рассматривались. Основные клиенты были - сельхозтехника, электротурбины и дизели, где обороты куда скромнее.
Если оставить за кадром всё остальное, то да.
Синхронизации и блокировки тормозят процесс работы с данными. Т.е. весь объём времени по сути делится между числом потоков, и каждый поток может работать с этими данными не больше, чем время, оставшееся от работы других потоков. А мне надо было, чтобы задержек не было, чтобы было быстро. Тут палка о двух концах - либо блокировки и синхронизации с неизбежным ожиданием очереди на работу с данными, либо одновременный доступ с осуществлением "потокобезопасности" другими способами, типа специальной организации доступа к данным, когда потоки не будут друг другу мешать. Сделать и то, и то одновременно невозможно. Разве что пойти способом, подобным рэйд-массивам в системах хранения данных - т.е. делать копии данных и возволять работать потокам с этими копиями. Но это надо делать систему синхронизации копий, а кроме того, у нас не получится чистый рэйд, т.к. физический источник будет всё равно один - общая RAM, ну и узким местом будет интерфейс доступа к этой RAM. Вобщем, выходы наверняка есть, но не тривиальные.
Кстати, во всяких веб-джаваскриптовых графиках я не встретил примера постоянно обновляемого графика, способного отрисовывать по хотя бы 10к точек, соединённых линиями, хотя бы 5 раз в секунду. Т.е. это не только проблема WPF с его retained model (когда данные, передающиеся в видеокарту, не изменяются, а если надо изменять, да ещё постоянно - производительность быстро падает), а в принципе сложно сделать такой высокопроизводительный график на высокоуровненых языках с кучей абстракций. Сколько ни оптимизируй, а всё равно упрёшься в систему отрисовки. Если не сможешь в неё вмешаться, то всё - остаются оптимизации лишь в твоём коде, поставляющем данные на отрисовку.
Это не интерпретация - это основы. А интерпретация это как реализовано с C#, Python или других языках или либах. Поэтому я и говорю, что лучше все-таки разобраться в основах, чем как это реализовано в каком-то языке. По крайней мере когда я въезжал в тему, мне так было существенно быстрее.
Вот ссылки: Coroutine, Event_loop
Die Sache ist, что они предоставляют набор правил ("делай так и не делай так") для обращения с чёрным ящиком async/await. А вы пытаетесь туда залезть и понять суть. Да ещё суть не конкретного чёрного ящика, а целой группы похожих чёрных ящиков. А суть в конкретном ящике могут со временем заменить, не заменив правил. И тогда понимание сути устареет. Тем более, понимание сути для всей группы ящиков.
Вы - разработчик на определённых уровнях абстракций. Как устроено внутри - не ваше дело. Если вам надо знать, как устроено внутри не из-за любопутства, а чтобы понять эти абстракции, значит абстракции плохие и их нужно заменить. Ну типа как управление автомобилем (пусть это будет ручная коробка и инжектор) - у всех примерно одинаково, с небольшими нюансами на холостые ходы педалей, руля, силы нажатия. Но суть одна, и научившись ездить на одном, вы почти так же поедете на другом. А если вам надо обязательно лезть под капот или разбираться с системой впрыска конкретного инжектора, чтобы просто поехать - нафиг такие абстракции. Я не имею ввиду, скажем, идеальные профили разгона и управления в заносах - это у каждой машины своё, и чтобы использовать её по максимому, надо к ней привыкать.
Но в любом случае, async/await не заменяет нормальную работу с потоками (Thread, lock, всякие MemoryBarrier и прочее).Ну понятно что не заменяет - это разные модели.
Новичку, да и середнячку сложно сразу понять, где заканчивается банальная асинхронность и начинается настоящая многопоточность. Многие новички пытаются писать на async/await код, который скорее выглядит как многопоточный, чем асинхронный, и проваливаются. Как я говорил, лучше в тьюториалах иметь более-менее исчерпывающий набор типичных use cases для async/await и за него сильно не выходить, чем просто вываливать на людей набор правил и делайте с ними что хотите. Создатели async/await наверняка сами прорабатывали эти use cases - для чего они вообще эти конструкции языка делают. Иначе это превращается в хождение по граблям даже для сеньёров, не то что новичков.
Это не интерпретация - это основы
откуда такая страсть к конспектированию первоисточников?
Зачем разбираться в чем что не имеет связи с конкретной имплементацией?
нельзя вызвать и получить значение
очень даже можно
Единственно что можно это добавить ее как таск в очередь
Нет в шарпе никаких очередей для этого, есть стейт машина.
Это явный сигнал диспетчеру
нет и специального диспетчера для асинков.
То есть всё, что касается шарпной имплементации совсем не соответствует действительности. Ну зачем такая теория,которая даже и не теория.
я не встретил примера постоянно обновляемого графика
https://arction.com/lightningchart-ultimate-sdk/
не пробовал, но пишут - allows rendering 16 billion data points simultaneously.
Чувствуется научный работник
Я к настоящей науке отношусь примерно как девушка, желающая похудеть, к олимпиадной лёгкой атлетике - ну т.е. тоже что-то там хожу-бегаю вокруг своего дома, как умею, чтобы вес сбросить. Но и только. )))
https://arction.com/lightningchart-ultimate-sdk/
не пробовал, но пишут - allows rendering 16 billion data points simultaneously.
Pricing - Purchase or renew LightningChart .NET subscription - Arction
От полутора килобаксов на одного разработчика. Понятно, что нас нищий научный институт не мог себе такого позволить. Судя по их описанию, они там жуткий "code to metal" использовали - наверняка переписали систему рендеринга для компонента, обходя встроенную в WPF.
Кроме того, есть рекомендация "For best performance in WPF and multithreading benefits, select Non-Bindable chart.". Чарт, насколько я знаю, это одно окошко с графиком - т.е. там все графики должны быть без привязок. Мне иногда нужно было добавить всякие "зоны" с подсветкой и привязками прямо на график. Так что самый быстрый и оптимальный вариант отпадает. А что там реально у них - ещё испытывать надо (30 дней дают). Но в любом случае, для коммерческого распространения - полтора килобакса на разработчика, а Dynamic Data Display бесплатен, т.к. его разработка оплачивалась Майкрософт по одной из их программ по развитию платформы.
У меня примерно так всё выглядело. График слева - "бегущий". По простому варианту вообще в 2-3 тыщи точек (index) укладывались, но это если с OBD по CAN-шине приходили уже посчитанные внутри мозгов движка обороты. Кстати, вспомнил, что эта прога была защищена ключём HASP - там дополнительные настройки и опции в программе скрывались, если специальный разрабский ключ не вставлен в USB. В настрйках дефолтные нереальные значения представлены (не вбиты параметры конкретного устройства), поэтому мощность маленькая.
Вот, кстати, как выглядит отчёт - генерится FlowDocument на WPF, затем сохраняется в XPS или PDF. Контролы с цветными полукругами и шкалами - самописные, с байндингами положений стрелочек по измеренным параметрам. По всему приложению поддерживается многолокальность, но переведено всё лишь на 2 локали - русскую и английскую. Весь отчёт заполняется по объекту результатов - отсылаю результаты во вью-модель отчёта, и дальше вяжу эту вью-моедль ко вьюхе FlowDocument, который затем сохраняется в файл. Ну и плюс результаты отсылаются нам на сервер в базу данных (через WCF service), где и сайт крутится (ASP.NET MVC), который может показать результаты по проведённым опытам и статистику. Единственное, я не разобрался с защитой сайта - ну, чтобы сервис работал через сертификаты. Не успел разобраться с собственным сертификационным центром и самоподписанными сертификатами, и как его к нашему сервису приделать.
Насчёт этой библиотеки графиков - они и для WPF с Win Forms поддерживают, а говорят, что это сейчас на рынке никому не надо и лишь старьё мамонта поддерживать. Но раз продают, занчит покупают для этих платформ, а значит, и новые разработки ведут. Просто на фоне орды джаваскриптеров и мобильных разработчиков это теряется.
Речь то шла прежде всего о быстром, а теперь уже значит и бесплатный.
Вроде рекламируют тоже
https://lvcharts.net/App/examples/Wpf/start
It depends on the case, In general the geared package is really fast, in the best case it can plot 10 million points in a second, but don't trust this, give it a try, you can easily test an application running LiveCharts.Geared by cloning this repository.
Зачем разбираться в чем что не имеет связи с конкретной имплементацией?
Чтобы потом не разбираться в 20 различных имплементациях. Асинхронное программирование и корутины существуют десятки лет, и каждый норовит ввести свои термины и свою специфику.
Нет в шарпе никаких очередей для этого, есть стейт машина.
Если нет очереди, то значит текущие запущенные корутины нигде не хранятся, а значит система и не может их выполнить (она про них ничего не знает). Значит ничего выполняться вообще не будет.
нет и специального диспетчера для асинков.
А кто тогда решает, что выполнять в следующий момент из нескльких альтернативных вариантов? Если никто не рашает (диспетчера ведь нет), то значит в C# можно выполнять только чисто последовательные программы.Классный язык - мне нравится.
И тогда понимание сути устареет.
-----
Как бы тебе по-проще это объяснить...
Устареет - занание конкретной имплементации.
Но конкретную имплементацию в деталях никто, ну кроме разработчиков, в голове не держит.
В голове держат какую-то модель... и не факт что модель станет неактуальной... а правильная модель - 100% останется...
чтобы просто поехать
-----
Пустыня. Куча дохлых машин. Все водители - передохли.
У тебя - выбор - присоеденится к водителям или найти подходящую систему впрыска для замены твоей сдохшей...
Это, кстати, довольно близкая аналогия к ситуации в программировании...
Нет в шарпе никаких очередей для этого
-----
Это не важно.
Важно что предложенная модель объясняет происходящее...
При компиляции требуется складывать и извлекать элементы в/из стека.
При написании методом рекурсивного спуска - никакого выделенного стека нет.
Но есть рекурсивный вызов процедуры, при котором создаются локальные переменные...
Так что, будем говорить что стек не нужен? и ничего что переменные обычно создаются на стеке.
чтобы просто поехать
-----
Пустыня. Куча дохлых машин. Все водители - передохли.
У тебя - выбор - присоеденится к водителям или найти подходящую систему впрыска для замены твоей сдохшей...
Это, кстати, довольно близкая аналогия к ситуации в программировании...
Я, наверное, сдохну - это проще. )))
При компиляции требуется складывать и извлекать элементы в/из стека.
При написании методом рекурсивного спуска - никакого выделенного стека нет.
Но есть рекурсивный вызов процедуры, при котором создаются локальные переменные...
Так что, будем говорить что стек не нужен? и ничего что переменные обычно создаются на стеке.
Не знаю, про какой стек вы речь ведёте, просто не так давно я давал ссылку на Эрика Липперта, который сказал в том смысле, чтобы мы, пользователи Сишарпа, не заморачивались, где реально создаются переменные - стек, куча или параллельная реальность, ибо это от нас не сильно зависит, и компилятор сам решает, где что создать. Если много переменных создавать, которые должны быть в стеке, а они туда не помащаются, то их создадут в куче.
Туда тоже ещё попасть надо. Тот же немецкий должен быть на хорошем уровне. Да банально надо несколько лет опыта, чтобы язык "примелькался", чтобы разные говоры, интонации, произнесённые при разных условиях, узнаваться начали.
У меня даже ощущение, что устроиться программистом с плохим немецким и даже английским (разговорный на уровне средненького В1, например), но с хорошими знаниями программирования (опять же, не отличными, а просто хорошими) проще, чем на кассу в Лидл.
Важно что предложенная модель объясняет происходящее...
Я как то не привык к сферическим коням в вакууме
ну вот например программка что была
private static async Task TestWithDirectAwait() { Wait1(); Console.WriteLine("after wait1"); await Wait2(); Console.WriteLine("after wait2"); await Wait3(); Console.WriteLine("after wait3"); }
Код сгенерированный компилятором был немного подработан, в итоге получился следующий вывод
***State Machine Main MoveNext -1
***State Machine TestWithDirectAwait MoveNext -1
after wait1
***State Machine Wait2 MoveNext -1
wait2 Start tread id 1 (Main1-1)
***State Machine Wait2 MoveNext 0
wait2 for 2000 is ready after 00:00:02.0550266 tid 4 (Main2-4)
***State Machine TestWithDirectAwait MoveNext 0
after wait2
***State Machine Wait3 MoveNext -1
wait3 Start tid 4 (Main2-4)
***State Machine Wait3 MoveNext 0
wait3 for 1000 is ready after 00:00:03.0750480 tid 6 (Main3-6)
***State Machine TestWithDirectAwait MoveNext 1
after wait3
***State Machine Main MoveNext 0
Summary time 00:00:03.0754335 thread Id 6
Вот объясни мне по модели, как это все работает. При этом у тебя есть только
- State Machine
- Synchronization context
- Execution Context
- ThreadPool
- TaskScheduler
Чего он в самом начале (слайд примерно на 8 минуте) взъелся на то, что для организации async/await что-то там потребляется - аж сотня-другая байт памяти и несколько десятков наносекунд (на каком железе ещё не упомянуто)? А оно должно в принципе бесплатным быть, или как?
Я почему-то думаю, что в каком-нибудь "асинхронном" джаваскрипте организация разных корутин стоит дороже. А создание потоков через Thread в Сишарпе - оно бесплатно?
Вобщем, непонятная претензия без сравнения с альтернативами на Сишарпе и с похожими вещами в других языках.
А ты упрости...
А там нечего убирать, вот часть оригинала. Приложение консольное.
Обыгрывается только лишь енто все остальное отладка
await Task.Delay(delay);
private static async Task TestWithDirectAwait() { Wait1(); Console.WriteLine("after wait1"); await Wait2(); Console.WriteLine("after wait2"); await Wait3(); Console.WriteLine("after wait3"); } private static void Wait1() { SetThreadName(); Console.WriteLine($"wait1 Start tid {Thread.CurrentThread.ManagedThreadId} ({Thread.CurrentThread.Name})"); int delay = 3000; Task.Delay(delay); SetThreadName(); Console.WriteLine($"wait1 for {delay} is ready after {DateTime.Now - _startTime} tid {Thread.CurrentThread.ManagedThreadId} ({Thread.CurrentThread.Name})"); } private static async Task Wait2() { SetThreadName(); Console.WriteLine($"wait2 Start tid {Thread.CurrentThread.ManagedThreadId} ({Thread.CurrentThread.Name})"); const int delay = 2000; await Task.Delay(delay); SetThreadName(); Console.WriteLine($"wait2 for {delay} is ready after {DateTime.Now - _startTime} tid {Thread.CurrentThread.ManagedThreadId} ({Thread.CurrentThread.Name})"); }
А там нечего убирать
-----
Да неправда ваша, батенька...
Бо, в "модель" ты накидал МНОГО, а в качестве обоснования что оно нужно привел код из которого нужность не вытекает... и даже как бы наоборот - код от "модели" получается полностью оторванным.
У Срыв покровов модель показывает как может работать async/await
Не говорю что оно работает именно так, этого от модели не требуется.
Говорю - оно МОЖЕТ работать так.
Причем описание - логично и связно.
Т,е, его легко: частью - запомнить, частью - восстановить.
И на его базе можно строить работоспособный код с использованием async/await.
Что, собственно, и требуется от модели.
Бо, в "модель" ты накидал
А я никогда и не предлагал модель. Есть только конкретный пример.
Не говорю что оно работает именно так
Нут так в том то и дело, на кой мне нужна модель которая которая не объясняет работу моего приложения, а только запутывает еще больше.
этого от модели не требуется.
Это может тебе не требуется, а мне требуется.
ваша тема - подтверждение тому, что "развитие языка программирования" в подавляющем большинстве приводят не к упрощению и удобству, а усложнение понимания кода. в самом безобидном случае - просто syntax sugar.
в с# уже имелись несколько вариантов реализации мультитрейдинга. вполне понятных уже десятилетиями и поколениями. кому не хватало еще этого? "никто не понимает, как оно работает и что будет, но зато не три, а две строчки".
Есть только конкретный пример.
Вот объясни мне по модели, как это все работает. При этом у тебя есть только
- State Machine
- Synchronization context
- Execution Context
- ThreadPool
- TaskScheduler
а мне требуется.
-----
Значит тебе надо поработать над пониманием того что такое модель...
Простейший вариант - идешь в магазин и покупаешь модельку ИЛ-96.
Потом объясняешь самому себе - почему вес не соответствует, почему она не планирует как написано в техдоках, почему грузоподъемность не та... и т.п.
Вот объясни мне по модели
Ты не правильно прочитал
Правильно будет - Вот объясни мне по "твоей" модели
над пониманием того что такое модель.
Скорее нужно определить случаи ее использования.
Возьми вот свой самолетик и учи на нем пилотов летать.
Зато другая модель отлично с этим справится, при этом и крылья и мотор нужны не будут.
Досмотрел это видео (только английский вариант). Где-то с 20 минуты перестал следить за кодом, что он приводит, и пытаться проверять. Что понял по сути, да и он сам в конце резюмировал - иногда в больших компаниях, типа Амазона, требуется делать сложные вещи и писать десяток асинхронных вызовов один из другого. Ну типа каверзных задачек, где замутят какую-нибудь структуру наследования позаковыристей, а я потом в голове скомпилировать это должен. Ну и таким компаниям тесно со всякими асинками-авейтами - они лезут в потроха и находят там то, что им не нравится. Тогда они начинают на любой чих подменять контексты синхронизации, авейтить события через пачки монад (которые не настоящие монады, а типа самописный фреймворк) и тому подобный лапшекод, который обычному разработчику явно чужд.
Вот он там приводит примеры, что если я сделаю так, а это объявлю так, а потом вызову это через это вот так, то можно поиметь проблемы. По-моему, подобным образом поиметь проблемы можно всегда и везде, а на одних асинках свет клином не сошёлся. Как я уже выше писал - можно и в наследованиях запутаться (потому, кстати, некоторые предпочитают "включения" - так называемое "has a", а не "is a" - та же компонентная система в Unity). Да, асинки не идеальны, но, как он же сам сказал, асинхронного фреймворка идеального нигде нет, а в благословенной джаве только-только якобы задумались, как к проблеме подойти - вот уже три года думают.
По видео этого парня, с асинками нельзя работать нормально, не обвешав их лапшой из тасования контекста синхронизации, самописных монад или костылей для сохранения всех выброшенных исключений, чтобы они не терялись. Печально...
Отдельно умилила фраза про фейсбуковцев, которые переписали строки в С++ и сэкономили на этом 1% производительности. Это якобы сэкономило ресурс сотен (или тысяч? - не помню) машин. Это, наверное, целые миллионы долларов в год, а может и пара десятков. В годовых отчётах Фейсбука подобные числа, наверное, даже не упоминаются, а тут целая команда работала, строки в С++ "ускоряла". Ну явно же парням хотелось премии, и надо было срочно что-то улучшить и отчитаться. А улучшить на 1% всегда найдётся что. Ну нет у ребят другой работы, кроме как по проценту тут или там что-то улучшать. Хорошо устроились. В обычной фирме скажи, что ты тут за месяц-два улучшил что-то узкоспециализированное на 1% - выпнут с работы. )))
Вобщем, как я понял с асинками, есть несколько простых случаев, где их можно использовать легко и приятно, если не выходить за строгие рамки контекста и его ограничений. Шаг влево, шаг вправо, прыжок в потроха фреймворка - лучше расстреляться на месте.
Вот объясни мне по "твоей" модели
-----
Ну так Срыв покровов это и сделал.
При том более чем удовлетворительно...
Возьми вот свой самолетик и учи на нем пилотов летать.
-----
А причем тут Я?
Ты сам выбирал и покупал (строил!) модель - вот теперь объясни почему на ней нельзя обучить пилота летать...
При том более чем удовлетворительно...
Ну так о чем и прошу, возьми эту модель и объясни отчего конкретный пример работает как работает.
Таски это то, что добавляется в event loop и там их куча накапливается. Выполняются они одним потоком, который в явно заданные моменты может перескакивать на другие таски.
Ты сам выбирал и покупал (строил!) модель
Я ничего не строил и не выбирал - это было твое предложение. Причем вне зависимости от того для каких целей мне хочется иметь модель.
сам рекламируешь
-----
Что именно Я "рекламирую"?
Я говорю, что то, что было представлено, вполне достаточно объясняет как используется указанное.
Насколько оно соответствует - не важно.
Можно ли приблизить к реальному положению вещей - ДА!
Будет ли приближенная к реальности модель сложнее - ДА!
Нужно ли усложнять модель - НЕТ - предложенная достаточна.
Тебе же хочется чтобы вместо предложенной была создана модель идеально отображающая существующее положение и пофиг что она получится настолько сложной, что для ее понимания потребуется строить модель модели...
Чего вы о моделях спорите? Вы лучше скажите, вы реально применяете асинки не так по-простому, как в тьюториалах в МСДН, а обмазываете подменой контекстов, добавлением монадоподобных самописных фреймворков и прочими вещами? Вроде, асинки не для того были сделаны, чтобы так сложно с ними обращаться? А получается чуть ли не то, что сделать всё через ручное создание потоков проще - хотя бы контролируешь больше и понимаешь, что происходит, а не магия какая-то под копотом. Ну или тупо старые паттерны применять с цепочкой вызовов "задача, задача после окочания задачи, задача, если произошло исключение". Но при приёме на работу-то сейчас все хотят, чтобы ты именно в асинках разбирался.
Насколько оно соответствует - не важно.
Это смотря для чего. Пойти с пацанами в войнушки с самолетиком поиграть, пойдет, возможно.
Но я не хочу иметь машинку с квадратными колесами.
была создана модель идеально отображающая существующее положение
Подобная модель не имеет смысла
для какой цели нужна модель
-----
У ТС определено что именно надо покрыть моделью...
Всё, что я понял - асинки это не обязательно мультипоток. И если будешь выё..., ну т.е. писать много лапши с асинками, то почти наверняка получишь дедлок. А все эти "тут захватываем контекст, тут не захватываем, тут в монады заворачиваем" - это для автомехаников 80 уровня, досконально знающих, что происходит под капотом.
Вон, даже Адам Фурманек подобное по бумажке читает (заранее подготовленным примерам с подсказками). ))) А я на собесах должен это всё из головы доставать? Да ещё спонтанно на рандомный вопрос?
Да и "в бою", сами понимаете, нет времени долго думать - по "2-3 задачи в минуту" надо решать. Майкрософт и все остальные в своих поделках не могут сделать нормальный асинхронный фреймворк, а джуниор-миддл Алекс должен вам тут за пять минут всё профессионально раскидать? ))
Как я понял, речь идёт о модели того, как внутри работают асинки. Ну и они работают явно на конкретной текущей реализации (чего там? стейт-машина?). Т.е. там нет какой-то общепринятой "теории асинхронного программирования", которой бы все реализации должны были следовать и выучив которую ты бы мог понимать любую из этих реализаций.
Вдобавок по тому видео с Адамом Фурманеком. Практически все вопросы, что он затронул, освещены в статье
Asynchronous programming - C# | Microsoft Docs
и ссылкам в ней. Разве что Адам предложил какие-то спагетти для решения отдельных проблем - может, у них в Амазоне так и делают. Но основные ловушки и рекомендации использования асинков в типичных случаях в статье по моей ссылке приведены. И про стейт-машину и прочее там тоже есть, но без потрохов. С потрохами - полно статей в инете. Вплоть от 17 года нашёл.