Последние два года я глубоко вовлечён в процесс написания многопоточного серверного кода. Это были интересные сложные задачи, которые не всегда удавалось решить удачно. Свои и чужие ошибки позволяют сделать выводы о проблемах, и способах их решения.
Проблемы
Доставшийся мне код страдал от двух главных проблем:
Поддержка кода. Любой плохо написанный код тяжело поддерживать. Зависимости между объектами, цикломатическая сложность, и т.д. бывает непростой для понимания задачей само по себе. Многопоточность добавляет новое измерение, усложняя код многократно. Если попытаться систематизировать проблемы с поддержкой, получится примерно такой набор:
-
Неструктурированная синхронизация – возникающие в неожиданных местах примитивы синхронизации, пытающиеся связать множество претендующих на ресурс потоков. Часто это выражается в попытки сделать блокировки более глобальными. Что с чем синхронизируется при этом понять довольно сложно.
-
Асинхронные вызовы через
APM/
TPL. Модель APM перегружена инфраструктурными примитивами, что сильно усложняет его понимание (кто не верит – пусть попробует написать собственный
IAsyncResult, и для окончательного счастья сделать вложенные вызовы APM). Смешение полезного кода с кодом APM делало код практически не читаемым. Использование TPL немного смягчало боль, но в целом не решало проблемы.
-
Сложности с тестированием. Написание тестов на многопоточный бизнес код совсем нетривиальная задача. Кроме сложности в понимании кода добавляются “радости” внезапных ошибок, пришедших из потока, запущенного совсем другим тестом (особый камень в огород таймеров).
Производительность. В большей части случаем по требуемой производительности сервисы работали неплохо, но иногда неприятности всё-таки случались. Краткий обзор проблем:
-
Каждый поток требует 1 Мб памяти для стека. Пока потоков мало – можно не беспокоится. Но когда в пиковых нагрузках количество потоков подскакивает до 1000 – это проблема.
-
Множество заблокированных потоков создают лишнюю нагрузку на планировщик потоков, увеличивают количество требуемых переключений контекста. Всё это создаёт бесполезную нагрузку на процессор.
-
Примитивы синхронизации Windows требуют перехода в режим ядра, даже если при этом не происходит ожидания и блокировки потока. Такие вызовы обходятся очень дорого. Кроме того, создание объектов ядра весьма дорого так же дорого. CLR например использует пулы объектов ядра для работы Monitor’а именно по этой причине.
-
Сборщик мусора останавливает (Suspend) все потоки, и сканирует их стеки в поисках более не используемых объектов. Надо ли говорить, что производительности это не добавляет?
Все это заставляло меня поминать чертей гораздо чаще, нежели я люблю при работе с кодом. Почувствовав себя усталым от тяжкого бремени всепроникающей многопоточности, я решил заняться поиском решения.
Решение
После долгих размышлений в голове прояснились контуры решения.
-
Избегать (в крайнем случае минимизировать) вторжение асинхронного кода в код приложения. Для случая асинхронного выполнения синхронной операции этот пункт мог бы решить даже
ThreadPool.QueueUserWorkItem, но при этом возникает проблемы обработки исключений и контроля уровня многопоточности. Для вызовов в стиле APM задача еще более усложняется, и окончательного решения у меня пока нет.
-
Избегать конкурентного доступа к ресурсам. Идею как этого достичь почерпнул из
node.js. Чтобы избегать синхронизации – не надо использовать конкурентный доступ к ресурсам. Это не означает использовать только один поток вообще, но один поток одновременно.
-
Не тратить понапрасну машинные ресурсы - не создавать лишние потоки (ThreadPool будет к месту, но использовать надо с хитростями), минимизировать использование примитивов синхронизации ядра, не блокировать потоки. Такая цель требует изменения подхода к проектированию, но вполне достигаема.
Эти идеи (частично) воплощены в проекте Codestellation DarkFlow. Пока он страдает отсутствием документации, но над этим недостатком я собираюсь в ближайшее время поработать. О его внутренностях и механизмах расскажу в следующей раз.
PS. Прощу прощения, но видимо предыдущая серия блогов про CardFlow останется незаконченной. Как всегда, времени на всё не хватает. Да и с тех пор успел изрядно подзабыть начатое.