Текущее асинхронное сообщение отброшено асинхронным диспетчером

Обновлено: 02.07.2024

Традиционно в программировании используют синхронное программирование — последовательное выполнение инструкций с синхронными системными вызовами, которые полностью блокируют поток выполнения, пока системная операция, например чтение с диска, не завершится. В качестве примера ниже написан echo-сервер:

При вызове методов read() и write() текущий поток исполнения будет прерван в ожидании ввода-вывода по сети. Причём большую часть времени программа будет просто ждать. В высоконагруженных системах чаще всего так и происходит — почти всё время программа чего-то ждёт: диска, СУБД, сети, UI, в общем, какого-то внешнего, независимого от самой программы события. В малонагруженных системах это можно решить созданием нового потока для каждого блокирующего действия. Пока один поток спит, другой работает.

Но что делать, когда пользователей очень много? Если создавать на каждого хотя бы один поток, то производительность такого сервера резко упадёт из-за того, что контекст исполнения потока постоянно сменяется. Также на каждый поток создаётся свой контекст исполнения, включая память для стека, которая имеет минимальный размер в 4 КБ. Эту проблему может решить асинхронное программирование.

Асинхронность

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

Callbacks

Для написания асинхронной программы можно использовать callback-функции (от англ. callback — обратный вызов) — функции, которые будут вызваны асинхронно каким-либо обработчиком событий после завершения задачи. Переписанный пример сервера на callback-функциях:

В wait_connection() мы всё ещё ждём чего-то, но теперь вместе с этим внутри функции wait_connection() может быть реализовано подобие планировщика ОС, но с callback-функциями (пока мы ждём нового соединения, почему бы не обработать старые? Например, через очередь). Callback-функция вызывается, если в сокете появились новые данные — лямбда в async_read() , либо данные были записаны — лямбда в async_write() .

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

У такого подхода есть несколько проблем. Первую в шутку называют callback hell. Достаточно погуглить картинки на эту тему, чтобы понять, насколько это нечитаемо и некрасиво. В нашем примере всего две вложенные callback-функции, но их может быть намного больше.

Async/Await

Пройдём по программе построчно:

  • Ключевое слово async в заголовке функции говорит компилятору, что функция асинхронная и её нужно компилировать по-другому. Каким именно образом он будет это делать, написано ниже.
  • Первые три строки функции: создание и ожидание соединения.
  • Следующая строка делает асинхронное чтение, не прерывая основной поток исполнения.
  • Следующие две строки делают асинхронный запрос в базу данных и чтение файла. Оператор await приостанавливает текущую функцию, пока не завершится выполнение асинхронной задачи чтения из БД и файла.
  • В последних строках производится асинхронная запись в сокет, но лишь после того, как мы дождёмся асинхронного чтения из БД и файла.

Это быстрее, чем последовательное ожидание сначала БД, затем файла. Во многих реализациях производительность async / await лучше, чем у классических callback-функций, при этом такой код читается как синхронный.

Корутины

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

Несколько точек входа

По сути корутинами называются функции, имеющие несколько точек входа и выхода. У обычных функций есть только одна точка входа и несколько точек выхода. Если вернуться к примеру выше, то первой точкой входа будет сам вызов функции оператором asynс , затем функция прервёт своё выполнение вместо ожидания БД или файла. Все последующие await будут не запускать функцию заново, а продолжать её исполнение в точке предыдущего прерывания. Да, во многих языках в корутине может быть несколько await ’ов.

Для большего понимания рассмотрим код на языке Python:

Программа выведет всю последовательность чисел факториала с номерами от 0 до 41.

Функция async_factorial() вернёт объект-генератор, который можно передать в функцию next() , а она продолжит выполнение корутины до следующего оператора yield с сохранением состояния всех локальных переменных функции. Функция next() возвращает то, что передаёт оператор yield внутри корутины. Таким образом, функция async_factorial() в теории имеет несколько точек входа и выхода.

Stackful и Stackless

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

Так как в корутинах мы можем в любом месте поставить оператор yield , нам необходимо где-то сохранять весь контекст функции, который включает в себя фрейм на стеке (локальные переменные) и прочую метаинформацию. Это можно сделать, например, полной подменой стека, как это делается в stackful корутинах.

На рисунке ниже вызов async создаёт новый стек-фрейм и переключает исполнение потока на него. Это практически новый поток, только исполняться он будет асинхронно с основным.

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


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

Более производительными, но вместе с тем и более ограниченными, являются stackless корутины. Они не используют стек, и компилятор преобразует функцию, содержащую корутины, в конечный автомат без корутин. Например, код:

Будет преобразован в следующий псевдокод:

По сути здесь создаётся класс, который сохраняет всё состояние функции, а также последнюю точку вызова yield . У такого подхода есть проблема: yield может быть вызван только в теле функции-корутины, но не из вложенных функций.

Симметричные и асимметричные

Корутины также делятся на симметричные и асимметричные.

Симметричные имеют глобальный планировщик корутин, который и выбирает среди всех ожидающих асинхронных операций ту, которую стоит выполнить следующей. Примером является планировщик, о котором говорилось в начале функции wait_connection() .

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

Вывод

Асинхронное программирование является очень мощным инструментом для оптимизации высоконагруженных программ с частым ожиданием системы. Но, как и любую сложную технологию, её нельзя использовать только потому, что она есть. Необходимо всегда задавать себе вопрос: а нужна ли мне эта технология? Какую практическую пользу она мне даст? Иначе разработчики рискуют потратить очень много сил, времени и денег, не получив никакого профита.

Хотя сочетание блокирующих и неблокирующих операций и метода select большую часть времени является достаточным для запросов к устройству, в некоторых ситуациях управление по методикам, которые мы видели до сих пор, не эффективно.

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

Пользовательским программам необходимо выполнить два шага, чтобы разрешить асинхронное уведомление от входного файла. Во-первых, они определяют процесс как "владельца" этого файла. Когда процесс вызывается командой F_SETOWN , используя системный вызов fcntl , process ID владельца процесса сохраняется для последующего использования в filp->f_owner . Этот шаг необходим для ядра, чтобы знать, кого уведомлять. Для того, чтобы действительно разрешить асинхронное уведомление, пользовательским программам необходимо установить в устройстве флаг FASYNC с помощью команды F_SETFL fcntl .

После выполнения этих двух вызовов входной файл может запросить доставку сигнала SIGIO при получении новой информации. Этот сигнал передаётся в процесс (или группу процессов, если значение отрицательное), хранящийся в filp->f_owner .

Например, следующие строки кода пользовательской программы разрешают асинхронное уведомление текущего процесса для входного файла stdin :

signal(SIGIO, &input_handler); /* пустышка; лучше использовать sigaction( ) */

fcntl(STDIN_FILENO, F_SETOWN, getpid( ));

oflags = fcntl(STDIN_FILENO, F_GETFL);

fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);

Программа в исходниках, названная asynctest , является простой программой, которая, как показано, считывает stdin . Она может быть использована для тестирования асинхронных возможностей scullpipe . Программа похожа на cat , но не прекращается при достижении конца файла; она реагирует только на ввод, но не на отсутствие ввода.

Однако, следует отметить, что не все устройства поддерживают асинхронное уведомление, и вы может принять решение не предлагать его. Приложения обычно предполагают, что асинхронные возможности доступны только для сокетов и терминалов.

Осталась одна проблема с уведомлением ввода. Когда процесс получает SIGIO , он не знает, какой входной файл предлагает новый ввод. Если более чем один файл имеет возможность асинхронно уведомить об этом процесс, ожидающий ввод, приложение должно по-прежнему прибегать к poll или select , чтобы выяснить, что произошло.

С точки зрения драйвера

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

1. При вызове F_SETOWN ничего не происходит, кроме того, что filp->f_owner присваивается значение.

2. Когда выполняется F_SETFL , чтобы включить FASYNC , вызывается метод драйвера fasync . Этот метод вызывается всякий раз, когда в filp->f_flags меняется значение FASYNC для уведомления драйвера об изменении, чтобы он мог реагировать должным образом. При открытии файла флаг по умолчанию очищается. Мы будем рассматривать стандартную реализацию метода драйвера далее в этом разделе.

Реализация первого шага тривиальна - со стороны драйвера ничего делать не надо, другие шаги включают в себя поддержание динамической структуры данных, чтобы отслеживать разных асинхронных читателей; их может быть несколько. Эта динамическая структура данных, тем не менее, не зависит от определённого устройства и ядро предлагает подходящую реализацию общего назначения, чтобы вам не пришлось переписывать тот же самый код в каждом драйвере.

Две функции, которые вызывает драйвер, соответствуют следующим прототипам:

int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);

void kill_fasync(struct fasync_struct **fa, int sig, int band);

fasync_helper вызывается, чтобы добавить или удалить записи из списка заинтересованных процессов, когда для открытого файла изменяется флаг FASYNC . Все эти аргументы, за исключением последнего, предоставляются методом fasync и могут быть переданы напрямую через него. kill_fasync используется, чтобы при поступлении данных просигнализировать заинтересованным процессам. Её аргументами являются сигнал для отправки (обычно SIGIO ) и диапазон, который почти всегда POLL_IN (* POLL_IN является символом, используемым в коде асинхронного уведомления; это эквивалентно POLLIN | POLLRDNORM.) (но это может быть использовано для передачи "срочно" или о наличии данных во вспомогательном канале в сетевом коде). Вот как реализуется метод fasync в scullpipe :

static int scull_p_fasync(int fd, struct file *filp, int mode)

struct scull_pipe *dev = filp->private_data;

return fasync_helper(fd, filp, mode, &dev->async_queue);

Понятно, что вся работа выполняется fasync_helper . Однако, было бы невозможно реализовать функциональность без метода в драйвере, так как вспомогательной функции требуется доступ к правильному указателю на структуру fasync_struct * (здесь &dev->async_queue ) и только драйвер может предоставить такую информацию.

Затем при поступлении данных, до посылки сигнала асинхронным читателям, должна быть выполнена следующая команда. Поскольку новые данные для читателя scullpipe порождаются процессом, делающим запись, эта команда выполняется в методе write драйвера scullpipe .

Заметим, что некоторые устройства также реализуют асинхронное уведомление, чтобы сообщить, когда устройство может быть доступно для записи; конечно, в этом случае kill_fasync должна вызываться с режимом POLL_OUT .

Может показаться, что мы всё доделали, но всё ещё отсутствует одна вещь. Мы должны вызвать наш метод fasync , когда файл закрыт для удаления этого файла из списка активных асинхронных читателей. Хотя этот вызов требуется, только если filp->f_flags имеет установленный FASYNC , вызов функции в любом случае не повредит и является обычной реализацией. Следующие строчки, например, являются частью метода release в scullpipe :

/* удалить этот filp из асинхронно уведомляемых filp-ов */

scull_p_fasync(-1, filp, 0);

Структура данных, лежащая в основе асинхронного уведомления, почти идентична структуре wait_queue , потому что обе эти ситуации связаны с ожиданием события. Разница в том, что вместо структуры task_struct используется структура file . Затем, чтобы послать сигнал процессу, для получения f_owner используется структура file из очереди.

Ранее помогал перезапуск следующих служб подряд

служба лицензирования, Локатор удаленного вызова процедур (RPC), служба посредник подключения к удаленному раб. столу, Перенаправитель портов пользовательского режима служб удаленных рабочих столов, Служба инициатора Майкрософт iSCSI(тоже после ребута сервера, другой сервак теряет диск ICSI, и пока службу не рестартнем, он его не увидит).

и после этого рестартим "Службы удаленных рабочих столов", если их рестартить сразу, без рестарта вышеуказанных. то вылетит ошибка, что служба не ответила вовремя. и в итоге она висит в состоянии остановка.

Если все это проделать, то доступ восстанавливается.

Пробовал делать DISM /Online /Cleanup-Image /RestoreHealth с указанием источника пакета wim (без него он не делал), писал что восстановлено, но проблема осталась.

Пробовал переустановить всю роль RDS. Проблема так же осталась. после перезагрузки приходится делать рестарт служб указанных выше.
Сегодня данный алгоритм не помог.
Если выставить уровень безопасности rdp, вместо "согласование", до доступ будет и без перезапуска служб.
Пришлось пока оставить уровень безопасности rdp, но в коллекциях я не вижу сеансы пользователей, хотя они зашли и работают.

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

Есть два метода, один из которых асинхронен. Main вызывается в новом потоке и вызывает асинхронный метод, который по окончанию заносит в поле результат выполнения.
В Main нужно дождаться выполнения MakeAnalysisRequest , после чего вернуть поле.

Нынешний метод Main при первом обращении возвращает пустое поле, при втором - результаты первого и так далее.
Вероятно, асинхронный MakeAnalysisRequest завершается позже, чем Main .
Как всё-таки заставить Main ожидать завершения второго метода?

1 ответ 1

Вам не должно хотеться синхронно дождаться окончания асинхронной операции. Это очень плохая идея, и нивелирует все достоинства async/await.

Кроме того, async void и общение функций через глобальное состояние (статическую переменную) — ещё одна крайне плохая идея. В любом случае, переделайте метод MakeAnalysisRequest , чтобы он возвращал не void :

Теперь по-хорошему ваш вызов должен выглядеть так:

В качестве временного хака, позволяющего использовать синхронный метод, может сработать такой отвратительный код (а может и привести к deadlock'у):

Читайте также: