Максимальное число потоков windows

Often I see people asking why they can’t create more than around 2000 threads in a process. The reason is not that there is any particular limit inherent in Windows. Rather, the programmer failed to take into account the amount of address space each thread uses.

A thread consists of some memory in kernel mode (kernel stacks and object management), some memory in user mode (the thread environment block, thread-local storage, that sort of thing), plus its stack. (Or stacks if you’re on an Itanium system.)

Usually, the limiting factor is the stack size.

#include <stdio.h>
#include <windows.h>

DWORD CALLBACK ThreadProc(void*)
{
 Sleep(INFINITE);
 return 0;
}

int __cdecl main(int argc, const char* argv[])
{
int i;
 for (i = 0; i < 100000; i++) {
  DWORD id;
  HANDLE h = CreateThread(NULL, 0, ThreadProc, NULL, 0, &id);
  if (!h) break;
  CloseHandle(h);
 }
 printf("Created %d threads\n", i);
 return 0;
}

This program will typically print a value around 2000 for the number of threads.

Why does it give up at around 2000?

Because the default stack size assigned by the linker is 1MB, and 2000 stacks times 1MB per stack equals around 2GB, which is how much address space is available to user-mode programs.

You can try to squeeze more threads into your process by reducing your stack size, which can be done either by tweaking linker options or manually overriding the stack size passed to the CreateThread functions as described in MSDN.

  HANDLE h = CreateThread(NULL, 4096, ThreadProc, NULL,
               STACK_SIZE_PARAM_IS_A_RESERVATION, &id);

With this change, I was able to squeak in around 13000 threads. While that’s certainly better than 2000, it’s short of the naive expectation of 500,000 threads. (A thread is using 4KB of stack in 2GB address space.) But you’re forgetting the other overhead. Address space allocation granularity is 64KB, so each thread’s stack occupies 64KB of address space even if only 4KB of it is used. Plus of course you don’t have free reign over all 2GB of the address space; there are system DLLs and other things occupying it.

But the real question that is raised whenever somebody asks, “What’s the maximum number of threads that a process can create?” is “Why are you creating so many threads that this even becomes an issue?”

The “one thread per client” model is well-known not to scale beyond a dozen clients or so. If you’re going to be handling more than that many clients simultaneously, you should move to a model where instead of dedicating a thread to a client, you instead allocate an object. (Someday I’ll muse on the duality between threads and objects.) Windows provides I/O completion ports and a thread pool to help you convert from a thread-based model to a work-item-based model.

Note that fibers do not help much here, because a fiber has a stack, and it is the address space required by the stack that is the limiting factor nearly all of the time.

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

Это перевод Does Windows have a limit of 2000 threads per process? Автор: Реймонд Чен. Альтернативный перевод (и да, я знаю, что мои переводы — отстой).

Я часто вижу, как люди спрашивают, почему они не могут создать больше (примерно) 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. Надеюсь, я не надоел вам своими примечаниями :) Что-то их получилось выше крыши в этом посте. Я обещаю стараться сводить их к минимуму.

Developers usually thinks that Windows has a default value for the max number of threads a process can hold. while in fact the limitation is due to the amount of address space each thread have can have.

I was working lately on some mass facility simulation applications to test some complex RFID based system. I needed to have a different thread for each person in the facility to simulate parallel and different movement of people at the same time .. and that’s when the fun started .with the simulation running more than 3000 individuals at the same time, it wasn’t too log before I got the OutOfMemory Unfriendly Exception.

When you create new thread, it has some memory in the kernel mode, some memory in the user mode, plus its stack, the limiting factor is usually the stack size. The secret behind that is the default stack size is 1MB and the user-mode address space assigned to the windows process under 32 bit Windows OS is about 2 GB. that allow around 2000 thread per process (2000 * 1MB = 2GB).

Hit The Limit!

Running the following code on my virtual machine with the specs in the image runs around ~ 1400 Threads, the available memory on the machine also takes place in deciding the Max number of threads can be allocated to your process.

Now running the same application multiple times on different machines with different configs will give a close or similar results.

using System;
using System.Collections.Generic;
using System.Threading;

namespace MaxNumberOfThreadsDemo
{
    public class Program
    {
        private static List<Thread> _threads = new List<Thread>();

        public static void Main(string[] args)
        {
            Console.WriteLine("Creating Threads ...");
            try
            {
                CreateThreadsWithDefaultStackSize();
                //CreateThreadsWithSmallerStackSize();
                //CreateThreadsWithSmallerThanMinStackSize();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }

            Console.ReadLine();
            Console.WriteLine("Cleanning up Threads ...");
            Cleanup();

            Console.WriteLine("Done");
        }


        public static void CreateThreadsWithDefaultStackSize()
        {
            for(int i=0; i< 10000; i++)
            {
                Thread t = new Thread(DoWork);
                _threads.Add(t);
                t.Name = "Thread_" + i;
                t.Start();
                Console.WriteLine(string.Format("{0} started!",t.Name));
            }
            
        }
        private static  void Cleanup()
        {
            foreach (var thread in _threads)
                thread.Abort();
            _threads.Clear();
            _threads = null;
        }

        private static void DoWork()
        {
            Thread.Sleep(Int32.MaxValue-1);
        }
    }
}

image

How Can you extend the Limit?

Well, There are several ways to get around that limit, one way is by setting IMAGE_FILE_LARGE_ADDRESS_AWARE variable that will extend the user mode address space assigned to each process to be 3GB on 32x windows or 4GB on 64x.

Another way is by reducing the stack size of the created thread which will in return increases the maximum number of threads you can create. You can do that in C# by using the overload constructor when creating a new thread and provide maxStackSize.

Thread t = new Thread(DoWork,256);

Beginning with the .NET Framework version 4, only fully trusted code can set maxStackSize to a value that is greater than the default stack size (1 megabyte). If a larger value is specified for maxStackSize when code is running with partial trust, maxStackSize is ignored and the default stack size is used. No exception is thrown. Code at any trust level can set maxStackSize to a value that is less than the default stack size.

Running the following same code after setting the maxStackSize of the created threads to 256KB will increase the number of max threads created per process.

image

Now If maxStackSize is less than the minimum stack size, the minimum stack size is used. If maxStackSize is not a multiple of the page size, it is rounded to the next larger multiple of the page size. For example, if you are using the .NET Framework version 2.0 on Microsoft Windows Vista, 256KB (262144 bytes) is the minimum stack size, and the page size is 64KB (65536 bytes). according to MSDN.

In closing, resources aren’t free, and working with multiple threads can be really challenging in terms of reliability, stability, and extensibility of your application. but definitely hitting the max number of threads is something you want to think about carefully and consider maybe only in lab or simulation scenarios.

More Information

  • MSDN: Thread
  • Memory Limits for Windows Releases
  • Does Windows have a limit of 2000 threads per process?
  • MaxNumberOfThreadsDemo Application

Hope this Helps,

Ahmed

Эксперимент

Создаем новое консольное приложение. Вставляем следующий код:

   private static void ThreadOverhead() {
      const Int32 OneMB = 1024 * 1024; // размер одного мегабайта
      using (ManualResetEvent wakeThreads = new ManualResetEvent(false)) {
         Int32 threadNum = 0;
         try {
            while (true) {
               Thread t = new Thread(WaitOnEvent);
               t.Start(wakeThreads);
               Console.WriteLine("{0}: {1}MB", ++threadNum,
                  Process.GetCurrentProcess().PrivateMemorySize64 / OneMB);
            }
         }
         catch (OutOfMemoryException) {
            Console.WriteLine("Out of memory after {0} threads.", threadNum);
            Debugger.Break();
            wakeThreads.Set();   // гасим все потоки
         }
      }
   }

   private static void WaitOnEvent(Object eventObj) {
      ((ManualResetEvent)eventObj).WaitOne();
   }

Запускаем ThreadOverhead на исполнение:

   public static void Main() {
      ThreadOverhead();
      }

И наблюдаем результат, для моей системы это выглядит так:

1333: 103MB
1334: 103MB
1335: 103MB
1336: 103MB
1337: 103MB
1338: 103MB
1339: 103MB
1340: 103MB
1341: 103MB
1342: 103MB
1343: 104MB
1344: 104MB
1345: 104MB
1346: 104MB
1347: 104MB
1348: 104MB
1349: 104MB
1350: 104MB
1351: 104MB
1352: 104MB
1353: 104MB
1354: 104MB
1355: 104MB
1356: 104MB
1357: 104MB
1358: 104MB
1359: 105MB
1360: 105MB
Out of memory after 1360 threads.

Кстати — обратите внимание как постепенно начинает «тормозить» программа. Чем ближе к концу свободной памяти и больше потоков — тем выше накладные расходы на выделение этой памяти и особенно на переключение контекста между потоками (Context switching).

А какая от этого польза?

Казалось бы — эксперимент «о сферических конях в вакууме», но не спешите с выводами. Так например если посмотреть разницу между масштабируемым и не масштабируемым web-сервером, то там возникает точно такая же ситуация, только потоки создаются не нами, а ThreadPool-ом web-сервера IIS от Майкрософт. И понимание как происходит перегрузка IIS блокированными потоками я надеюсь многим поможет избежать этой ситуации в продакшене.

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Как посмотреть скрытые файлы на windows 7 на диске
  • Виртуалка windows для мак
  • Как открыть msg файл в windows 10
  • Через что смотреть фото на windows 10
  • Как проверить скорость загрузки windows 10