Ja, jeder Thread hat seinen eigenen Stack. Das ist eine unabdingbare Notwendigkeit, denn auf dem Stack wird gespeichert, wohin eine Methode nach ihrer Beendigung zurückkehrt, er speichert die Rücksprungadresse. Da jeder Thread seinen eigenen Code ausführt, braucht er seinen eigenen Stack. Lokale Variablen und Methodenargumente werden ebenfalls dort gespeichert, was sie (normalerweise) thread-sicher macht.
Die Anzahl der Haufen ist ein komplizierteres Detail. Sie zählen 1 für den Garbage Collected Heap. Das ist aus Sicht der Implementierung nicht ganz korrekt, denn die drei Generationsheaps und der Large Object Heap sind logisch getrennte Heaps, so dass sich die Zahl auf vier erhöht. Dieses Implementierungsdetail wird wichtig, wenn Sie zu viel zuweisen.
Ein weiterer Punkt, den man in verwaltetem Code nicht ganz ignorieren kann, ist der Heap, in dem statische Variablen gespeichert werden. Er ist mit der AppDomain verbunden, statische Variablen leben so lange, wie die AppDomain lebt. In der .NET-Literatur wird er häufig als "Loader Heap" bezeichnet. Er besteht eigentlich aus 3 Heaps (Hochfrequenz-, Niederfrequenz- und Stub-Heap), auch Jitted Code und Typdaten werden dort gespeichert, aber das ist nur ein kleiner Teil der Details.
Weiter unten auf der Ignorierliste stehen die von nativem Code verwendeten Heaps. Zwei davon sind in der Marshal-Klasse leicht zu erkennen. Es gibt einen Standard-Prozess-Heap, aus dem Windows alloziert, ebenso Marshal.AllocHGlobal(). Und es gibt einen separaten Heap, in dem COM Daten speichert; Marshal.AllocCoTaskMem() belegt ihn. Schließlich hat jeder native Code, mit dem Sie interagieren, seinen eigenen Heap für seine Laufzeitunterstützung. Die Anzahl der Heaps, die von dieser Art von Code verwendet werden, ist nur durch die Anzahl der nativen DLLs begrenzt, die in Ihren Prozess geladen werden. Alle diese Heaps sind vorhanden, Sie haben nur selten direkt mit ihnen zu tun.
Also, mindestens 10 Haufen.