Letztes Jahr, Produktionssystem bei einem Maschinenbauer aus dem Schwäbischen. 180 Mitarbeitende, ERP-nahe .NET-Eigenentwicklung, läuft seit acht Jahren. Der zuständige Entwickler zeigt mir stolz eine neu eingebaute ConcurrentDictionary-Lösung für einen In-Memory-Cache von Artikelstammdaten. Der Code sieht aus wie aus einem Tutorial abgeschrieben — was er tatsächlich war. Funktioniert. Keine Abstürze. Kein offensichtlicher Fehler.
Aber Resharper wirft in derselben Klasse an drei Stellen die Warnung "Closure can be eliminated" — und niemand weiß, was das bedeutet, ob es wichtig ist, und ob man die drei Stunden in die Behebung investieren soll oder lieber in das offene Ticket zur Lieferschein-PDF-Generierung, die seit Wochen flackert.
Genau für diesen Moment ist Khalid Abuhakmeh's Artikel geschrieben.
Was der Artikel sagt
Khalid erklärt zunächst sauber, was eine Closure ist: eine Funktion, die Variablen aus ihrem umliegenden Scope "einfängt" — und dadurch zusätzliche Heap-Allokationen verursacht, potenziell thread-unsichere Referenzen hält und im schlimmsten Fall Memory Leaks produziert. Dann zeigt er das konkrete Problem bei ConcurrentDictionary.GetOrAdd: Wenn man im valueFactory-Lambda eine externe Variable verwendet — etwa einen Konfigurationswert oder einen Parameter — entsteht automatisch eine Closure. Die saubere Lösung ist ein wenig bekannter Overload der Methode, der einen dritten Parameter factoryArgument akzeptiert. Damit übergibt man den Zusatzwert explizit, die Closure entfällt, und die Allokation auch. Der Artikel ist präzise, korrekt, und für .NET-Entwickler, die schon wissen was ConcurrentDictionary ist, sofort umsetzbar.
Wie das in einem KMU mit 30 bis 300 Mitarbeitenden wirklich landet
Ehrliche Antwort: bei den meisten meiner Mandate gar nicht — weil niemand weiß, ob die drei Stellen mit der IDE-Warnung überhaupt unter Last laufen.
Das ist keine Kritik am Artikel. Das ist die Realität. Khalid schreibt für ein Publikum, das bei JetBrains-Tooling aufmerkt, Duende-Codebasen kennt und die Unterschiede zwischen GetOrAdd-Overloads aus Interesse nachschlägt. Das ist legitim. Aber wenn ich bei einem Metallbauer in Heilbronn oder einem Logistikdienstleister in Hamburg sitze, ist der Entwickler — oft einer von zweien — froh, wenn die IDE-Warnung überhaupt jemand ernst nimmt.
Was ich in solchen Situationen tue: erst messen, dann urteilen.
Die Frage ist nicht "Ist Closure-Vermeidung gut?" — die Antwort ist ja, generell schon. Die Frage ist: Wird dieser Code-Pfad unter Last ausgeführt? Ein ConcurrentDictionary als Cache für Artikelstammdaten, der beim Start einmal befüllt wird und dann nur noch gelesen wird, produziert durch eine Closure keinen messbaren Schaden. Der valueFactory-Lambda wird schlicht nie aufgerufen, sobald der Key existiert. Die Warnung ist technisch korrekt — aber die Priorität ist null.
Anders sieht es aus, wenn der ConcurrentDictionary als Session-Cache in einer ASP.NET-Core-Anwendung lebt, die unter Last 50 parallele Requests pro Sekunde abarbeitet, jeder Request neue Keys generiert, und der Lambda bei einem Cache-Miss immer feuert. Dann reden wir über reale Allokationen, über GC-Pressure, und über einen Effekt, den man in dotMemory oder dem eingebauten .NET-Profiler tatsächlich sehen kann.
Der Unterschied: Im ersten Fall bringt die Korrektur des Codes primär Ordnung — und Ordnung ist gut, aber eben nicht dringend. Im zweiten Fall ist es messbare Performance-Arbeit.
Was im Artikel fehlt — aus KMU-Sicht
Khalid setzt voraus, dass du weißt, wann ConcurrentDictionary das richtige Werkzeug ist. Das ist keine Selbstverständlichkeit.
In gut der Hälfte meiner Mandate, wo ich ConcurrentDictionary im Produktionscode antreffe, wäre Dictionary mit einem ganz normalen lock-Block die bessere Wahl gewesen — weil der Zugriff tatsächlich nur aus einem einzigen Thread erfolgt, weil die Datenstruktur nur beim Start geschrieben und danach ausschließlich gelesen wird, oder weil die Entwickler ConcurrentDictionary einfach als "den sichereren Dictionary" verstanden haben und damit lock sparen wollten. Das geht nach hinten los, denn ConcurrentDictionary hat eigene Fallstricke — zum Beispiel bei zusammengesetzten Operationen, die nicht atomar sind.
Kurz gesagt: Bevor du den factoryArgument-Overload verwendest, stell dir die Frage, ob ConcurrentDictionary an dieser Stelle überhaupt gerechtfertigt ist.
Meine konkrete Empfehlung
Wenn du .NET-Entwickler im Team hast und der Artikel in deinen Feed gespült wird:
Schritt 1: Zeig ihn deinem Team. Der Artikel ist gut und kurz. Wer täglich ConcurrentDictionary anfasst, soll den Overload kennen.
Schritt 2: Macht einen kurzen Review-Pass durch eure Codebase — gezielt für Stellen, wo GetOrAdd unter echtem Schreibdruck läuft. Nicht jede IDE-Warnung, sondern die, die in Hot Paths leben.
Schritt 3: Wenn ihr nicht wisst, was ein Hot Path in eurer Anwendung ist — das ist das eigentliche Problem. Ein 30-minütiger Profiling-Durchlauf mit dem .NET-eigenen dotnet trace oder mit dem integrierten Visual Studio Profiler bringt euch mehr als jedes Code-Review zur Closure-Hygiene.
Und wenn das Ticket zur flackernden PDF-Generierung noch offen ist: das zuerst. Fachliche Bugs kosten euch Vertrauen beim Kunden. Eine suboptimale Closure in einem selten gerufenen Lambda kostet euch vielleicht drei Millisekunden GC-Pause pro Stunde. Die Priorität ist klar.
Das ist übrigens keine Gegnerschaft zum Artikel — sondern die Ergänzung, die in einem .NET-Developer-Advocacy-Kontext nicht nötig ist, in einem KMU-Kontext aber die entscheidende Frage ist: Lohnt sich das gerade?
Der vollständige Original-Beitrag von Khalid Abuhakmeh: The Curious Case of .NET ConcurrentDictionary and Closures
Hast du in deiner .NET-Codebase ähnliche IDE-Warnungen, die ihr nicht priorisieren könnt — oder weißt du schlicht nicht, welche davon in euren Hot Paths landen? Dann lass uns das in einem kurzen Gespräch klären.
— Bernhard