C ++: обработка нескольких входных источников, выходных флагов и флагов исключений, а также таймеров из нескольких потоков - PullRequest
1 голос
/ 01 апреля 2020

Я программирую однопоточные приложения, управляемые событиями, в течение 30 лет.

Вообще говоря, это делается путем 1) установки всех операций ввода-вывода на неблокирующую, а затем 2) написания оболочки select(), poll() или аналогичные, которые поддерживают очередь событий таймера и массивы файловых дескрипторов, которые должны отслеживаться на предмет читабельности, возможности записи и исключения. Среди прочего, X Windows Toolkit Intrinsics (Xt) и библиотека XView предоставляют такие обертки, как Reuters RTR и ряд других источников.

В результате получается однопоточное приложение, которое, тем не менее, может отслеживать произвольное количество дескрипторов файлов (таких как сеансы TCP, пользовательские события из оконной среды и т. д. c.) вместе с большим набором таймеров. Если сокет становится читаемым (то есть, присутствует больше данных), он читается. Если он становится доступным для записи (то есть кэш записи полностью или частично очищается), можно записать больше данных и т. Д.

Теперь я пишу тот же тип программного обеспечения в многопоточной среде. .

Очевидно, что одним из подходов является блокировка ВСЕГО ввода-вывода и выделение отдельных потоков для чтения и записи каждого TCP-соединения. Точно так же потоки могут спать до тех пор, пока они не будут разбужены таймерами (я полагаю, возможно, но как?). Однако рабочая нагрузка программиста на настройку всех этих потоков и обеспечение доступа к общим переменным с помощью блокировок или операций atomi c, которые будут работать во всех ситуациях, кажется хуже, чем подход старой школы.

Другая крайность заключается в том, чтобы поддерживать одиночный выбор, который может быть чем-то вроде узкого места, но был бы наиболее знакомым для меня и позволял максимально использовать старый код. Вот пример того, почему его просто нельзя использовать как в многопоточном мире. Скажем, я хотел, чтобы таймер отключился на go через 1 секунду, но select() блокируется на 2 секунды для его следующего события таймера, и нет никакой другой файловой активности, которая иначе вызвала бы возврат select(). Мой 1-секундный таймер выключился бы go в самое раннее через 2 секунды, и это если я запишу время суток, в которое я получил запрос. (Если я наивно подчинялся 1-секундному времени обратного вызова, когда обрабатывал сообщение, оно вместо go отключалось бы через 3 секунды вместо 1 запрошенного.)

В качестве обходного пути я могу создать канал и зарегистрируйте один конец для отслеживания входящих данных с помощью select(), как и любой дескриптор файла. Потоки, отличные от основного потока, просто пишут свой запрос на регистрацию или удаление таймера или источника ввода в канал. Эти входящие данные активируются select (), и основной поток может (почти) сразу же добавить их в свою очередь таймера или добавить / удалить дескриптор файла из массива отслеживаемых данных, например, c.

* 1021. * Это также имеет некоторые острые проблемы, например, регистрация таймера становится асинхронной. Если вы передадите ссылку на объект, для которого требуется таймер, а затем захотите удалить этот объект до того, как таймер сработает, вы не знаете, можете ли вы его освободить, поскольку таймер еще не может быть зарегистрирован, и Отмена регистрации также будет асинхронной. Допустим, у меня есть приемлемые решения для всего этого (например, просто не удалять такие объекты).

Но я уверен, что между этими двумя крайностями используются некоторые стандартные подходы. Какими они могут быть? Может быть, так же просто, как иметь верхний l oop на основе select() для каждого потока, которому нужны таймеры или события ввода / вывода?

...