Выделяется ли куча на каждый поток?

Ответ

Нет. В .NET управляемая куча (Managed Heap) является общим ресурсом для всех потоков в рамках одного домена приложения (AppDomain).

  • Единая куча: Все потоки выделяют память для объектов (через new) в одной и той же куче.
  • Потокобезопасность выделения: Для предотвращения коллизий CLR использует внутренние механизмы синхронизации. Например, для быстрого выделения памяти в куче для малых объектов используется потоковый локальный кэш (Thread Local Allocation Buffer, TLAB), что минимизирует блокировки.
  • Индивидуальный стек: Каждый поток имеет свой собственный стек (stack) для хранения локальных переменных и контекста вызовов методов.

Пример и последствия:

// Этот список создается в общей куче
List<int> sharedList = new List<int>();

Task.Run(() => sharedList.Add(1)); // Поток 1
Task.Run(() => sharedList.Add(2)); // Поток 2
// Оба потока работают с одним объектом в куче -> требуется синхронизация!

Ключевое следствие: Поскольку куча общая, доступ к одним и тем же объектам из разных потоков требует применения примитивов синхронизации (lock, Mutex, SemaphoreSlim и т.д.) для обеспечения потокобезопасности.

Ответ 18+ 🔞

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

Представь себе огромный общий стол. Это и есть управляемая куча (Managed Heap). Все потоки в твоём приложении — это как обжоры за этим столом. Когда ты пишешь new Something(), поток такой: «Ага, мне надо место!» — и хватает кусок памяти со стола. Все жрут с одного стола. Нет отдельных тарелочек «для потока №1» и «для потока №2» в плане самой кучи.

Теперь, чтобы эти обжоры не начали драться из-за последнего куска пирога (памяти) и не устроили бардак, система придумала хитрость. У каждого потока есть своя маленькая личная хлебница (Thread Local Allocation Buffer, TLAB). Пока он берет память оттуда — всё быстро и без очередей. Но когда хлебница пуста, он всё равно идет к тому самому общему столу (куче), и там уже включается внутренняя магия CLR с легкими блокировками, чтобы два потока не впендюрили данные в одно место. Это сделано для скорости, чтобы не синхронизировать каждый чих.

А вот стек (stack) — это вообще отдельная песня. Это как личный блокнот у каждого потока в кармане. Туда он записывает локальные переменные, кто кого вызвал и прочую служебную хуйню. К этому блокноту другие потоки доступа не имеют — это твоё личное дело, как ты там свои int i = 5 записываешь.

И вот главный подвох, из-за которого всё летит в тартарары: раз куча общая, то если два потока начинают тыкаться в один и тот же объект в этой куче — это пиздец, прости мой французский. Смотри, как просто это сломать:

// Создаём один список. Он лежит на том самом общем столе (в куче).
List<int> sharedList = new List<int>();

// Запускаем двух обжор-потоков
Task.Run(() => sharedList.Add(1)); // Поток 1: "О, список! Кину туда 1."
Task.Run(() => sharedList.Add(2)); // Поток 2: "О, тот же список! Кину туда 2."

// И тут может случиться дичь: состояние гонки, повреждение внутренних полей списка,
// или вообще исключение. Потому что они лезут в одну кучу без спроса.

Вывод, который бьёт по башке: раз объект в общей куче, то доступ к нему из нескольких потоков нужно явно синхронизировать. Без lock, SemaphoreSlim, Mutex или других примитивов — будет боль, слёзы и нестабильная работа. Стек — твой личный, а куча — на всех. Помни это, и тогда многих проблем удастся избежать, как говна в луже.