Оооо, дак вот оно как работает await в с#, а вы безусловно по ходу нашего разговора именно это пытались объяснить с самого начала, показывая, какой вы знаток работы с#, что знаете, как N тасков "мапятся" на M тредов (M < N) в этом языке.
Теперь, когда я вижу вашу "компетенцию" в этом вопросе, мне абсолютно понтяно, что смысла идти с вами в этот "долгий путь" нет. Все последующие ваши высеры в данном контексте буду оставлять без комментария, ибо нефиг перечить знатоку с#.
Вообще я тут подумал, мне стало и так +/- понятно на мой вопрос ответ (на вопрос: "как же стейт машина в с# обрабатывает запросы, если там нет poll/select/epoll/kqueue и каких-то других схожих примитивов") и без замечательного специалиста по работе c#. Напишу для интересующихся, которые проглотили заманчивый высер, а в итоге получили фигу.
Краткий ответ - никак. Такого рода примитив должен быть, а так же должен быть этот цикл while явно или неявно, т.е. взять ваш код на c#, а потом при компиляции сделать вокруг него класс с полями, где каждая переменная будет свойством класса и вызовы await в этом коде делят его на состояния, в которое он попадает замечательным switch (state) case "x": ...
ничего не решает. Более подробно ниже, кому инетерсны умозаключения.
Представим код, который пытается отдать управление с помощью слов await/yield в многопоточном коде. Yield в данном случае будет являться не генератором, а методом или спец. функцией для планировщика задач, которая просит планировщика сменить задачу на данном потоке, так как текущая задача по каким-то причинам хочет уступить выполнение (ждёт ответа из сети или просто "хочет отдохнуть":-)). В этом случае поток выполнения будет переключаться на другой таск. А теперь представим, что тасков больше нет, т.е. вот у вас 2 таска, которые чего-то ждут по сети/с диска. Если вы будете переключаться между этими тасками, то будете получать так же 100% CPU load и не важно, стейт машина там или нет, ведь выполнять всё равно код каждого стейта дальше нельзя, так как данные не готовы. Что делать в этом случае? Скорей всего нужно иметь очередь задач, ожидающих выполнение + массив задач, которые suspended (ожидают чего-то), а так же спец. задачу, цель которой как раз и сделать этот вызов poll/select/... Назовём эту задачу так: X (для простоты написания монолога). Соответственно задача, которая suspended, отсутствует в очереди на выполнение, а задача X должна каким-то образом планироваться всё время. Вопрос в том, кто планирует задачу X? Самое простое решение такое: пусть она сама себя планирует, т.е. в конце эта задача сама себя помещает в очередь на выполнение. В итоге мы имеем этот цикл while, только неявно. Итого, что у нас имеется:
- таски, которые выполняется тогда, когда готовы
- пул потоков
- таски брать на выполнение может любой поток, который закончил выполнять у себя текущий таск
- особый таск Х, который занимается проверкой готовности ответа из сокета и планирует задачу, которая связана с этим сокетом, а так же планирует себя
- распределение N задач по M тредам
Таск Х может иметь такую логику, в дополнении к логике, которая есть в цикле событий простого планировщика, как в mojo:
можно не делать вызов poll с sleep_timeout > 0, если есть в очереди на выполнения задачи. В данном случае сделать = 0, позволяя просто узнать, какие таски можно вернуть в очередь на выполнение, и пойти "разгребать" очередь задач на выполнение.
Почему же я решил, что вместо epoll/select/... и цикла while нет ничего другого? Ну скажем так, я за долгое существование c#/go/rust и других не слышал других механизмов и системных вызовов, а значит с большой вероятность механизм тот же. Всё равно должен быть блокирующий вызов, который не тратит ресурсы системы в пустую, блокируясь, когда в очереди на выполнение нет задач.