Фиксированное количество нитей в многонитевой системе не есть уж очень хорошо. Зачастую не угадаешь пиковую нагрузку, а, если число реквестов превысит число нитей — то в некоторых случаях, когда обслуживание реквеста требует блокирующего надолго вызова — возникнет узкое место.
Потому используются динамические пулы нитей.
Как правильно имплементировать сие диво (если кому надо будет)?
Нить в данном случае есть ресурс. Если нить начала исполнять реквест — то количество доступных (ждущих реквеста на потребляющем конце очереди) нитей уменьшилось, возможно — до нуля.
Так вот, это "до нуля" тут же создает накопление реквестов в очереди, причем только потому, что еще не успели породить нить для их обслуживания, и реквест ждет либо такого порождения, либо пока закончится обслуживание еще какого-то реквеста. Второе — потеря скалабильности.
Потому разумно вот что: после того, как пуловая нить сняла реквест с очереди, она должна взглянуть на счетчик доступных нитей и, если он упал ниже какого-то значения, _тут же рожать еще одну нить_, выполняя реквест уже после этого. CreateThread — это достаточно быстро, по сравнению с, например, дисковой операцией или тем более SQL query.
В итоге накопление большой очереди надолго на пуле практически невозможно, по крайней мере если не ограничивать сверху число пуловых нитей. Как только наложен такой лимит — см. мои же посты о лимитах. При нагрузке большей, чем лимит — потеря скалабильности. При меньшей же нагрузке вообще все равно, есть лимит или нет.
Таковой лимит — как и остальные лимиты — имеет смысл только из соображений ограничения нагрузки на машину в целом. Кстати, основная нагрузка, создаваемая нитью — это кернел- и юзер- стеки.
Естественно, что имеет смысл сделать самоумирание нитей по таймауту в случае, если долго не было никакой нагрузки и так много нитей не нужно. Для этого ожидание на потребляющем конце очереди должно быть с таймаутом, и, если он наступил — то проверяется счетчик нитей, если он выше минимума — нить умирает.
Главное, думаю, понятно, остальное все напишут сами.
P.S. Ex/IoQueueWorkItem в ядре туп до блеска. Находясь под файловой системой, им пользоваться просто нельзя. Нельзя сделать queue work item из нити, которая сама есть work item — а путь записи на диск зачастую зовется из work item слива кэша.
Дедлоки из этой имплементации летят только так.
Занимайтесь LoveCraftом, а не WarCraftом!
Re: Performance & Scalability пост №4: thread pools
Здравствуйте, Maxim S. Shatskih, Вы писали:
MSS>Кстати, основная нагрузка, создаваемая нитью — это кернел- и юзер- стеки.
Для пуловых нитей можно вполне установить по-умолчанию размер стека порядка 64k. С возможностью пользователю перекрыть это значение.
Для большинства операций этого вполне достаточно, особенно учитывая, что пуловая нить выполняет только одну какую-то операцию, типа запроса SQL.
При этом заметно снижается потребление адресного пространства процесса.
Здравствуйте, Maxim S. Shatskih, Вы писали:
MSS>P.S. Ex/IoQueueWorkItem в ядре туп до блеска. Находясь под файловой системой, им пользоваться просто нельзя. Нельзя сделать queue work item из нити, которая сама есть work item — а путь записи на диск зачастую зовется из work item слива кэша. MSS>Дедлоки из этой имплементации летят только так.
Расшифруйте, пожалуйста.
Re[2]: Performance & Scalability пост №4: thread pools
MSS>>P.S. Ex/IoQueueWorkItem в ядре туп до блеска. Находясь под файловой системой, им пользоваться просто нельзя. Нельзя сделать queue work item из нити, которая сама есть work item — а путь записи на диск зачастую зовется из work item слива кэша. MSS>>Дедлоки из этой имплементации летят только так.
_>Расшифруйте, пожалуйста.
Ex/IoQueueWorkItem проимплементирован плохо, и склонен к дедлокам. Там ограничено общее количество нитей, там часто возникает дедлок, если выполнение одного work item завязано на ожидание выполнения другого work item, и так далее.
Так как Cache Manager и файловые системы активно пользуются этим пулом для lazy writing и кое-каких своих нужд, запросы к дисковому стеку (под файловой системой) зачастую приходят в контексте вот этого work item. Если дисковый фильтр хочет воспользоваться этим же пулом для каких-то своих нужд — велика вероятность дедлока.
Занимайтесь LoveCraftом, а не WarCraftом!
Re: Performance & Scalability пост №4: thread pools
Прежде чем реализовывать хитрые динамические пулы нужно сперва просто разделить потоки хотя бы на две части — жрущие процессор и не жрущие, и развести их по разным пулам.