Category Archives: С/C++

eventfd в Linux

Written by elwood

При разработке многопоточных приложений часто применяется следующий паттерн для синхронизации потоков : создается массив сигнализирующих объектов и вызывается wait для них всех, который блокирует выполнение потока и ожидает, пока один из этих объектов не даст сигнал. В это время другие потоки что-то делают, и рано или поздно сигнал вызывается. После этого wait завершает ожидание, возвращая индекс сработавшего объекта-события. Далее производятся какие-то действия по обработке этого конкретного события, и wait вызывается снова. В псевдокоде это выглядит примерно так:

event[] events = initializeEvents();
while (true) {
    int index = wait(events, TIMEOUT);
    if (0 == index) {
	// wait finished by timeout
    } else if (-1 == index) {
	// wait finished with error
    } else {
	event signalledEvent = events[index];
	// todo : working with signalled event
 
	if (signalledEvent == EXIT_EVENT) {
	    break;
	}
    }
}

Это очень удобный паттерн, который можно применять как в синхронизации потоков (различные очереди сообщений или просто разные действия), так и в обработке ввода (в Windows функции ожидания можно применить к STD_INPUT_HANDLE, поместив STD_INPUT_HANDLE в один массив с другими объектами-событиями).

И вот мне нужно было сделать аналогичное решение для обработки консольного ввода в Linux.

Была найдена функция poll(), которая работает с массивом файловых дескрипторов. Однако, мьютекс в этот массив засунуть нельзя, а отдельный файл или pipe создавать для этого дела не хочется. Должен же быть некий объект ядра, который будет достаточно легковесным для использования в этом сценарии ! И действительно, такой объект нашелся. Им оказался eventfd – сравнительно недавно появившийся в Linux API (первая версия появилась в ядре 2.6, а последние изменения в API были добавлены в версии 2.6.30).

Итак, как же работает eventfd. Обычный объект eventfd (не-семафорный, то есть не созданный с флагом EFD_SEMAPHORE) работает достаточно просто. Он содержит в себе 64-битный счетчик (изначально равный нулю) и позволяет писать в себя вызовом write() и читать вызовом read(). При записи (поддерживается только запись 64-битного значения) записываемое значение добавляется к счетчику. То есть вызвали 3 раза запись числа 10 – счетчик будет содержать значение 30. При вызове read() это значение будет считано, а счётчик вновь сброшен в ноль. Когда счетчик содержит 0, вызов poll с флагом POLLIN будет ожидать до тех пор, пока кто-то не запишет в eventfd значение. Если же счетчик содержит 1, вызов poll с флагом POLLIN вернёт управление сразу (раз есть, что считывать – eventfd “доступен для чтения”). Причем, так как при вызове poll() счётчик не сбрасывается, повторные вызовы будут тут же возвращать управление до тех пор, пока кто-то не вызовет read() и не сбросит тем самым значение внутреннего счетчика в ноль. Вызов же poll() с флагом POLLOUT будет, как правило, всегда возвращать true для флага POLLOUT (поскольку запись в него в принципе всегда разрешена, за исключением сценариев, при которых eventfd может быть заблокирован из-за другого активного в этот момент писателя).

Ну и так как eventfd представляет собой файловый дескриптор, связанный с объектом ядра, то его нужно закрывать вызовом close().

Итак, чтобы сделать аналогичную схему, нам достаточно простого алгоритма :

int fd = eventfd(0, EFD_CLOEXEC); // пусть это событие сигнализирует нам о продолжении некой работы
int fd2 = eventfd(0, EFD_CLOEXEC); // а это - о необходимости выйти из цикла приёма сообщений
pollfd pollfds[2];
pollfds[0].fd = fd;
pollfds[1].fd = fd2;
pollfds[0].events = POLLIN;
pollfds[1].events = POLLIN;
 
bool exitFlag = false;
while (!exitFlag) {
    int pollResult = poll(pollfds, 2, -1);
    if (pollResult == -1) {
	// handle error
    } else if (pollResult > 0) {
	// сработало одно или несколько событий - но без проверки revents -
	// флагов событий, действительно сработавших - нам не узнать индексы сработавших событий
	for (int i = 0; i < 2; i++) {
	    if (pollfds[i].revents != 0) {
		if (i == 0) {
		    // делаем работу, относящуюся к первому событию
		} else if (i == 1) {
		    exitFlag = true;
		    break;
		}
		// обнуляем внутренний счетчик, чтобы следующий вызов poll снова ожидал записи в eventfd
		uint64_t u;
		read(pollfds[i].fd, &u, sizeof(uint64_t));
	    }
	}
    }
}
 
...
 
// а тут где-то в другом потоке вызываем сигналы вот так
uint64_t u = 1;
write(fd, &u, sizeof(uint64_t));

При необходимости можно усовершенствовать схему, заменив третий аргумент poll с -1 (TIMEOUT_INDEFINITE) на конкретный таймаут и добавив обработку того случая, когда pollResult равен нулю (что как раз соответствует возврату по таймауту).

В общем, мы видим, что в принципе, ничего сложного в синхронизации через файловые дескрипторы нет, и можно легко построить цикл обработки сообщений из других потоков / процессов по аналогии с тем, как это привыкли делать в Windows с помощью объектов ядра Event и функций WaitForSingleObject / WaitForMultipleObjects.

PS. Возможно, производительность этого способа может быть хуже, чем при использовании мьютексов. Поэтому если производительность критична, наверное, стоит задуматься о том, как реализовать аналогичное поведение на POSIX мьютексах. На гитхабе есть проект https://github.com/NeoSmart/PEvents – он как раз про это (правда, там довольно много кода по сравнению с приведённым сниппетом).

Немного о фреймворках

Written by elwood

Обычно считается, что фреймворки для разработки полезны тем, что предоставляют возможность программисту пользоваться некой готовой инфраструктурой. То есть основной плюс – в том, что ДОБАВЛЯЮТСЯ ВОЗМОЖНОСТИ использовать готовое. Но мне в последнее время кажется, что главное преимущество даже не в этом, а в том, что фреймворки, как правило, помимо дополнительных возможностей также накладывают существенные ограничения на код, который будет в рамках него работать.

Пример – Spring MVC предлагает только несколько стандартных способов написать обработчик запроса. Хочешь обработать запрос – пропиши маппинг и добавь метод в контроллер. Хочешь датабиндинг – используй @ModelAttribute. Почему это хорошо ? Потому что разработчику нужно меньше шевелить мозгами, раздумывая над тем, как бы лучше запрограммировать этот кусочек. Больше времени останется на продумывание более важных вещей. Плюс к этому, все, кто знакомы со Spring MVC, будут с первого взгляда понимать то, что хотел сказать программист Вася, написавший этот код года два назад. Значит, добавление ограничений положительно влияет на процесс разработки ? Получается, так.

Действительно, имея широкий набор возможностей, тяжело научиться их правильно использовать. Причем, как правило, сначала учишься использовать правильно, набивая шишки, и попутно придумываешь правила сам. Некий кодстайл, паттерны для того, чтобы писать корректный код, не думая о деталях. Так я учился в своё время языку С++. Читая описание того, что такое header-файл, я нигде не мог найти аргументированных правил по его использованию. Сейчас такая литература появилась в изобилии, но в то время у меня еще не было интернета. И я долгое время не знал, как оформлять h-файлы и как их инклудить в cpp – что именно должно быть в h-файле, а что – в cpp, можно ли инклудить h-файл в другой h-файл; если cpp-файл использует h-файл, зависящий от другого, то нужно ли в cpp-файл включать эту зависимость итд. Поэтому сидел и морщил ум на тему, как же сделать правильно. А всё потому, что С++ предоставляет множество способов сделать это НЕправильно, а очевидности, очевидные для опытных программистов, далеко не так очевидны для новичков. Аналогичные шишки приходилось набивать и на других платформах. C#, к примеру, заставил меня призадуматься о стратегии обработки исключений и особенно о корректной реализации IDisposable в иерархии классов.

Поэтому хороший фреймворк должен не только предоставлять пачку возможностей, но и набор несложных правил о том, как, собственно, писать код. Чтобы лезть в декомпилятор/исходники/дебагер приходилось как можно реже. Codestyle & FAQ – идеальные форматы для такой информации.

minifmod4net

Written by elwood

Не так давно мутил я на дотнете воспроизведение трекерных файлов, а именно, XM. Громоздкие либы типа BASS явно для этой задачи не подходили, широко известный в узких кругах ufmod почему-то в связке с дотнет интеропом не заработал, зато нашлась библиотечка minifmod, которая обладала всеми необходимыми качествами (интересовала возможность загрузки из файла и из памяти) и небольшими габаритами. Для того, чтобы можно было удобно указывать способ загрузки (из файла или из памяти), я дописал маленький адаптер, который обрабатывал callback’и минифмода, и засунул его в DLL. Соответственно, нативная DLL к .net-проекту была подцеплена через простейший interop.

Сегодня наконец собрался с мыслями, почистил файлы проекта, и выложил все на гуглокод, благо sourceforge и codeplex уже попробовал. Google code очень понравился своей беспроблемностью в залитии файлов и работе с SVN. Вспоминая, как я трахался с sourceforge через SFTP чтобы залить туда пару архивов, могу сказать, что здесь все очень и очень удобно.

Итак, сайт проекта находится здесь http://code.google.com/p/minifmod4net/

Код использования библиотеки прост как пять копеек, достаточно взглянуть на тестовый пример, чтобы все стало ясно. Только один момент – если вы желаете использовать ее в своей программе, то вам придется сменить режим генерации кода с AnyCPU на x86, поскольку имеется нативная DLL с 32-битным кодом, и в режиме AnyCPU на какой-нибудь 64-битной винде программа не запустится, мотивировав отказ исключением наподобие ImageBadFormatException. Если же х86 специфицировать явно, то все будет работать и на 64-битных Windows.

PS. Поздравляю всех с наступившим 2010 годом, надеюсь, он принесет всем нам немало радости и профессиональных успехов 🙂