Я часто вижу, как люди спрашивают, почему они не могут создать больше (примерно) 2'000 потоков в процессе. Причина не в каком-то специальном ограничении в Windows. Просто программист забыл принять во внимание размер адресного пространства процесса, используемого каждым потоком.
Поток состоит из памяти в ядре (стеки ядра и управление объектом), памяти в пользовательском режиме (TEB (Thread Environment Block), TLS (Thread Local Storage) и подобные вещи), плюс его стек (или стеки, если вы работаете на процессорах Itanium).
Обычно ограничивающим фактором является размер стека:
program Project1; {$APPTYPE CONSOLE} uses Windows, SysUtils; function ThreadProc(const Arg: Pointer): Cardinal; stdcall; begin Sleep(INFINITE); Exit(0); end; var i: Integer; id: Cardinal; h: THandle; begin i := 0; while True do begin h := CreateThread(nil, 0, @ThreadProc, nil, 0, id); if h = 0 then Break; CloseHandle(h); Inc(I); end; WriteLn(Format('Created %d threads', [i])); end.Эта программа обычно печатает значение около 2'000 (прим.пер.: если вы хотите повторить этот эксперимент, то вам лучше бы запускать программу вне отладчика; также, запуск программы на x64 даст вам совсем другие числа - из-за вмешательства слоя эмуляции 32-х разрядных процессов).
Почему она сдаётся на числе 2'000?
Потому что размер резервируемого адресного пространства для стека потока по-умолчанию равен 1 Мб, а 2'000 стеков по 1 Мб равняются примерно 2 Гб - именно столько доступно коду пользовательского режима.
Вы можете попробовать втиснуть больше потоков в ваш процесс уменьшением начального размера стека - вы можете сделать это либо указанием размера в заголовке PE-файле, либо вручную указав размер стека в функции CreateThread, как указано в MSDN:
h := CreateThread(nil, 4096, @ThreadProc, nil, STACK_SIZE_PARAM_IS_A_RESERVATION, id);С этим изменением я смог создать около 13'000 потоков (прим.пер.: у меня получилось 30'469 - это и есть ожидаемое число: около 30'000; у Реймонда же сработало другое ограничение, а не стек; см. также ссылку на статью Марка Руссиновича в конце поста). Хотя это определённо лучше, чем 2'000, это меньше наивного ожидания 500'000 потоков (примерно столько влезет кусков по 4 Кб в 2 Гб). Потому что вы забыли о других накладных расходах. Гранулярность выделения адресного пространства - 64 Кб, так что каждый стек занимает 64 Кб адресного пространства, даже хотя он использует всего 4 Кб (прим.пер.: вот откуда число в примерно 30'000). Плюс, конечно же, у вас нет полностью свободных 2 Гб. Часть уже занята под системные DLL и прочие штуки.
Но настоящий вопрос, который встаёт, когда кто-то спрашивает: "Какое максимальное количество потоков я могу создать" - это: "Почему вы создаёте так много потоков, что это становится проблемой?"
Модель "один клиент - один поток" хорошо известна тем, что не масштабируется выше примерно дюжины клиентов. Если вы собираетесь обслуживать большее число клиентов одновременно, то вам лучше бы использовать другую модель, при которой вместо выделения потока клиенту вы просто создаёте объект (когда-нибудь я буду размышлять о двойственности между потоками и объектами). Windows предоставляет вам порты завершения ввода-вывода и пулы потоков, чтобы помочь вам перейти от модели клиент-поток к модели клиент-задача.
Заметьте, что волокна (fiber) не очень-то тут помогут, потому что у волокна тоже есть стек, а почти всё время ограничивающим фактором является именно стек.
Примечания переводчика:
1. Рекомендую почитать ещё статью Марка Руссиновича.
2. Вообще по теме серверных приложений и обслуживания клиентов я рекомендую отличную книгу от небезызвестного Джеффри Рихтера.
3. Надеюсь, я не надоел вам своими примечаниями :) Что-то их получилось выше крыши в этом посте. Я обещаю стараться сводить их к минимуму.
Примичаниями не надоел! Кому нужно мимо глаз их пропустит))
ОтветитьУдалить