Страницы

суббота, 30 марта 2013 г.

Codestellation DarkFlow. Укрощение многопоточности.

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

Проблемы

Доставшийся мне код страдал от двух главных проблем:

Поддержка кода. Любой плохо написанный код тяжело поддерживать. Зависимости между объектами, цикломатическая сложность, и т.д. бывает непростой для понимания задачей само по себе. Многопоточность добавляет  новое измерение, усложняя код многократно. Если попытаться систематизировать проблемы с поддержкой, получится примерно такой набор:

  • Неструктурированная синхронизация – возникающие в неожиданных местах примитивы синхронизации, пытающиеся связать множество претендующих на ресурс потоков. Часто это выражается в попытки сделать блокировки более глобальными. Что с чем синхронизируется при этом понять довольно сложно.
  • Асинхронные вызовы через APM/TPL. Модель APM перегружена инфраструктурными примитивами, что сильно усложняет его понимание (кто не верит – пусть попробует написать собственный IAsyncResult, и для окончательного счастья сделать вложенные вызовы APM). Смешение полезного кода с кодом APM делало код практически не читаемым. Использование TPL немного смягчало боль, но в целом не решало проблемы.
  • Сложности с тестированием. Написание тестов на многопоточный бизнес код совсем нетривиальная задача. Кроме сложности в понимании кода добавляются “радости” внезапных ошибок, пришедших из потока, запущенного совсем другим тестом (особый камень в огород таймеров).

Производительность. В большей части случаем по требуемой производительности сервисы работали неплохо, но иногда неприятности всё-таки случались. Краткий обзор проблем:

  • Каждый поток требует 1 Мб памяти для стека. Пока потоков мало – можно не беспокоится. Но когда в пиковых нагрузках количество потоков подскакивает до 1000 – это проблема.
  • Множество заблокированных потоков создают лишнюю нагрузку на планировщик потоков, увеличивают количество требуемых переключений контекста. Всё это создаёт бесполезную нагрузку на процессор.
  • Примитивы синхронизации Windows требуют перехода в режим ядра, даже если при этом не происходит ожидания и блокировки потока. Такие вызовы обходятся очень дорого. Кроме того, создание объектов ядра весьма дорого так же дорого. CLR например использует пулы объектов ядра для работы Monitor’а именно по этой причине.
  • Сборщик мусора останавливает (Suspend) все потоки, и сканирует их стеки в поисках более не используемых объектов. Надо ли говорить, что производительности это не добавляет? 

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

Решение

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

  • Избегать (в крайнем случае  минимизировать) вторжение асинхронного кода в код приложения. Для случая асинхронного выполнения синхронной операции этот пункт мог бы решить даже ThreadPool.QueueUserWorkItem, но при этом возникает проблемы обработки исключений и контроля уровня многопоточности. Для вызовов в стиле APM задача еще более усложняется, и окончательного решения у меня пока нет.
  • Избегать конкурентного доступа к ресурсам. Идею как этого достичь почерпнул из node.js. Чтобы избегать синхронизации – не надо использовать конкурентный доступ к ресурсам. Это не означает использовать только один поток вообще, но один поток одновременно.
  • Не тратить понапрасну машинные ресурсы -  не создавать лишние потоки (ThreadPool будет к месту, но использовать надо с хитростями), минимизировать использование примитивов синхронизации ядра, не блокировать потоки. Такая цель требует изменения подхода к проектированию, но вполне достигаема.

Эти идеи (частично) воплощены в проекте Codestellation DarkFlow. Пока он страдает отсутствием документации, но над этим недостатком я собираюсь в ближайшее время поработать. О его внутренностях и механизмах расскажу в следующей раз.

PS. Прощу прощения, но видимо предыдущая серия блогов про CardFlow останется незаконченной. Как всегда, времени на всё не хватает. Да и с тех пор успел изрядно подзабыть начатое.