вторник, 23 июня 2009 г.

Lock-free queue в модели MPSC

Сразу хочу оговориться, что в этой заметке я не ставлю себе цель рассказать, о всевозможных реализациях lock-free очереди, а только об одном способе, который я использовал в ядре библиотеки acto.

MPSC (multiple producers single consumer, «много производителей один потребитель») — модель взаимодействия в многопоточной среде, когда одновременно произвольное количество потоков могут добавлять элементы в очередь (или другую структуру), но только один поток может извлекать их.


Постановка задачи

Создать структуру данных, которая бы обеспечивала механизм FIFO (очередь) для MPSC модели взаимодействия потоков без использования блокировок. Существенное требование — реализация должна быть максимально проста и понятна.

Реализация

Во-первых, стоит сказать, что реализация lock-free очереди в общем виде ведет к необходимости решить так называемую ABA-проблему. Существуют различные способы решения данной проблемы, но следует признать, что для человека непосвященного они не являются ни простыми, ни понятными, поэтому мне хотелось найти другой способ, тем более что мне не требовалось обобщенную очередь, а достаточно, чтобы она работала в MPSC модели. Поэтому я стал искать другой способ, и нашёл, что с помощью двух стеков можно получить именно то, что требуется. Тем более, что в библиотеке acto уже была реализация lock-free стека пригодного для использования в модели MPSC.

Рассмотрим как использовать пару стеков, чтобы эмулировать поведение очереди. Допустим имеется два стека S1 и S2. Возьмем последовательность чисел A1 = [1, 2, 3, 4], помещая их в стек S1, а потом извлекая из него получим новую последовательность A2 = [4, 3, 2, 1]. Однако, если поместить A2 в стек S2 и затем извлечь её из него, то получим новую последовательность A3 = [1, 2, 3, 4]. Видно, что A3 == A1.

Нужно обратить внимание, что в реальном приложении после того, как цепочка элементов A2 будет помещена в S2 в стек S1 может прийти новая цепочка элементов B1 = [5, 6, 7]. Поэтому необходимо определить очередность добавления и извлечения элементов из обоих стеков. Очевидно, что если вынуть B1 из первого стека и добавить во второй до извлечения A2, то состояние второго стека будет следующим: S2 = [top: 5, 6, 7, 1, 2, 3, 4] и извлекая из него присутствующие элементы мы не получим исходного порядка поступления элементов. Поэтому, чтобы сохранить правильный порядок элементов необходимо всегда извлекать из S2 все элементы, прежде чем добавить новые.

Теперь рассмотрим особенности реализации стеков S1 и S2. Напомню, что в MPSC только один поток извлекает данные из стека, поэтому распределим обязанности между S1 и S2 следующим образом: S1 — разделяемый между потоками стек, использующий механизм lock-free, а S2 — локальный стек, в который будут складываться элементы, извлеченные из первого. Стоит обратить внимание, что извлечение элементов из S1 не обязательно делать по одиночке, а можно сразу же извлекать всю накопившуюся цепочку, что значительно уменьшает количество операций с разделяемой структурой данных. Так как S2 — локальный стек, то только один поток имеет к нему доступ и для него нет необходимости использовать какие либо блокировки вообще.


Итоги

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

На основе теста ping-pong в старой реализации (с использованием мьютекса для синхронизации доступа к очереди) средняя производительность была на уровне 730 тыс. сообщений в секунду (Intel Core2Duo, 4Gb RAM Windows Vista 32-bit). После того, как индивидуальная очередь сообщений объекта была переведена но lock-free реализацию производительность увеличилась до 830 тыс. сообщений в среднем. В относительных цифрах это соответствует увеличению производительности на 12%.

Однако стоит отметить, что возможна ситуация, когда данный алгоритм может оказаться медленее, чем обычный алгоритм очереди с блокировкой. Дело в том, что перекладывание элементов из S1 в S2 требует O(n) операций, где n – количество элементов в S1.

Для того, чтобы ознакомиться с исходным кодом, реализующим вышеописанный алгоритм, вы можете скачать последнюю версию библиотеки acto, но если вы читаете этот текст, когда с момента публикации прошло достаточно много времени, вам стоит обратиться к svn-репозиторию и посмотреть реализацию версии 0.2.7, которую можно найти в каталоге «tags/0.2.7»

среда, 17 июня 2009 г.

Новая версия библиотеки acto

Давно я ничего не писал, но это не означает что всё остановилось. Вчера я выложил новую версию библиотеки acto.

Основное нововведение в данном релизе — возможность собирать библиотеку под GNU/Linux. Протестировано на Ubuntu 8.04 (32-bit) и Ubuntu 9.04 (64-bit). Также есть некоторые нововведения в функциональности.

  • Когда это логически целесообразно, актер может самостоятельно завершить свою работу, вызвав метод this->terminate(), в процессе обработки поступившего сообщения. Также, хочу напомнить, что имеется функция acto::destroy(actor_ref), которая позволяет принудительно завершить работу любого актера, если у вас есть действительная ссылка на него.

  • В дополнение к двум вышеприведённым функция завершения работы актера, появился функция ожидания завершения работы актера acto::join(actor_ref). Её целесообразно использовать в рамках основного потока выполнения программы. Пример использования можно посмотреть в проекте ping-pong.

  • Возможность привязывать обработку сообщений актера к текущему системному потоку. Например, к главному потоку выполнения программы. Для этого необходимо указать специальный флаг во время создания актера — acto::instance_t(acto::aoBindToThread). При этом сообщения будут обрабатываться в рамках того потока, в котором актер был создан, и только при вызове функции acto::process_messages().

  • Для ускорения процесса пересылки сообщений появилась возможность создавать объекты-классы сообщений, например — acto::message_class_t msg_ball_class; Дело в том, что в текущей реализации при каждом вызове функции send(msg_ball()) происходит обращение к глобальному словарю, чтобы по типу сообщения получить уникальный идентификатор. Объект-класс запоминает данный идентификатор однократно, и в дальнейшем позволяет получать его без обращения к глобальному словарю. Для этого вызов метода send должен быть сделан следующим образом send(msg_ball_class.create()).

Вроде ничего существенного не забыто. Оставайтесь на связи.

вторник, 27 ноября 2007 г.

Новая версия библиотеки act-o

После достаточно продолжительного перерыва, я рад сообщить, что выложил новую версию библиотеки act-o.

Надо сказать, что до этого на sourceforge.net находилась концептуальная версия библиотеки – у ней был проработан интерфейс, можно было описывать взаимодействие актеров, можно было запускать программы на ее основе, но библиотека не могла продолжительное время работать под нагрузкой. Данный же релиз обладает полноценной средой выполнения (runtime) и планировщиком потоков.

Особо важным считаю тот факт, что в то время как среда выполнения была почти полностью переработана, интерфейс библиотеки остался неизменным. Это означает, что летний – проектный этап был успешным. Хотя некоторые косметические изменения все же пришлось внести.
  • Изменился заголовочный файл, который необходимо подключать, чтобы воспользоваться библиотекой – вместо «act_o.h» теперь необходимо подключать «multiprog.h».
  • Переименовано пространство имен – вместо act_o теперь необходимо писать acto (без подчеркивания).
  • Были добавлены две обязательные функции, которые необходимо явно вызывать acto::startup – инициализация библиотеки и acto::shutdown – закрытие библиотеки и освобождение всех ресурсов.
Возможности среды выполнения

Новая реализация runtime’а обладает следующими характеристиками:
  • Позволяет указывать объекты, сообщения для которых будут обрабатываться в отдельном потоке (эксклюзивные объекты) и объекты, потоки под которые будут распределяться автоматически (управляемы объект).
  • Подстраивается под загруженность системы и может выделять дополнительные потоки под управляемые объекты, если текущие потоки не справляются с обработкой всех сообщений. Это позволяет продолжать обработку сообщений, даже если часть объектов выполняет сверхдолгие или блокирующие операции. В данной версии установлено ограничение на максимальное число потоков – 512.
  • Автоматически удаляет излишние потоки, выделенные под управляемые объекты, при уменьшении нагрузки на систему.
  • Использует подсчет ссылок для определения того момента, когда объект можно удалять, но так как большинство объектов имеют ссылки на самого себя, то необходимо явно указывать момент удаления объекта с помощью функции acto::destroy. Однако следует отметить, что такой механизм не порождает проблем с повисшими ссылками, так как я позаботился о том, чтобы заголовок объекта удалялся только тогда, когда на него больше не остается ссылок. Некоторые причины такого способа управления объектов я описал ранее в своей заметке «Необходимость сборки мусора в многопоточных программах».
Тест работоспособности

Для того чтобы убедиться, что среда выполнения работает исправно, я провел тест на основе программы «ping pong», которая поставляется вместе с библиотекой. Вы также можете запустить ее у себя и сравнить результаты.

Тестирование проводилось со следующими параметрами: 50000 активных объектов, 50000 одновременно пересылаемых сообщений, 50 циклов start/stop в каждом из которых по 50 циклов активного обмена сообщениями. Код, который выполнялся в данном тесте, можно посмотреть, загрузив пакет библиотеки act-o, в файле «samples/ping-pong/main.cpp».

Тестовая платформа: ASUS P5K-VM, Intel Core Duo E2180, 2GB – DIMM PC-5300 DDR II, Windows XP x64 Pro.

Были получены следующие результаты – на ненагруженной машине средний показатель производительности составил 400’000 сообщений/сек. При тестировании было замечено, что среда выполнения не очень устойчива к сверхбольшой нагрузке – больше 100’000 активных объектов и больше 100’000 одновременно пересылаемых сообщений. Конечно, программа не падает, но работает очень медленно и в заданный интервал времени не успевает обработать все сообщения. Я думаю, что это в основном связано с отсутствием специально адаптированного менеджера памяти. Также в текущем алгоритме планирования есть вероятность избыточного расходования системных потоков – данный эффект может проявляться при очень плотном потоке сообщений.

Использование и дальнейшее развитие

На данный момент я применяю библиотеку act-o для создания серверных приложений. Если быть точнее, то для программирования асинхронной работы внутри сетевых серверов. Это позволяет мне легко организовать асинхронное взаимодействие между объектами приложения в случае использования неблокирующих сокетов.

Библиотека сетевых компонентов находится на ранней стадии производства и еще сложно сказать насколько может различаться производительность приложений сделанных на основе act-o и приложений, где многопоточность реализуется «руками». Пока что могу выразить только свое субъективное впечатление по поводу упрощения организации асинхронного взаимодействия между объектами и избавления от проблем с разделяемой памятью – ну почти как в Erlange… ;-)

Касательно планов дальнейшего совершенствования библиотеки act-o, то в первую очередь, я хочу снабдить ее полноценным многопоточным менеджером памяти, и добавить некоторые дополнительные возможности. Некоторые из них уже разработаны – например, таймер, который может посылать другим пользовательским объектам сообщение msg_time через указанные промежутки времени. В данный же релиз библиотеки он не включен потому, что его интерфейс еще недостаточно продуман и могут быть внесены изменения, которые сделают его несовместимыми с предыдущими версиями.

В более отдаленных планах еще раз переписать runtime уже на основе более проработанной модели планирования обработки сообщений и, возможно, снабдить его алгоритмом сборки мусора. Как и обычно менять интерфейс библиотеки при этом не планируется. Только возможно, что удастся избавиться от явного удаление объектов функцией acto::destroy, или как минимум сделать ее необязательной.

Вообщем, буду рад узнать, если библиотека act-o поможет вам при разработке ваших приложений. Со мной всегда можно связаться через этот блог, или написать прямо по почте – адреса можно найти в профиле или в заголовках к коду библиотеки.