DevOps

.NET 11 Process API: Deadlock’suz Süreç Yönetimi Geldi

Açık konuşayım, System.Diagnostics.Process sınıfı yıllardır.NET tarafında en sevdiğim ama en çok da sinirlendiğim sınıflardan biri (yanlış duymadınız). Hani şu “çalışıyor ama neden çalıştığını tam bilmiyorum” hissi var ya, işte tam orası. Logosoft’ta bir bankacılık tarafında geçen yıl bir batch işlemcisi yazıyorduk; RedirectStandardOutput ile WaitForExit‘i yan yana koyunca süreç askıda kalmıştı, klasik pipe buffer deadlock’u, üç saatimi yedi. Kötüydü.

Şimdi.NET 11 ile bu sınıfa yıllardır beklediğim ciddi bir el atılmış. Adam Sitnik ve ekip, sadece kozmetik bir dokunuş yapmamışlar; taban seviyede yenilemişler, bazı yerlerde insanın “oh be” dediği şeyler var, bazı yerlerde de “bunu daha önce niye yapmamışız” sorusu kafayı kurcalıyor. Buyurun bakalım tek tek gidelim — ama önce şunu söyleyeyim: bu yazıyı okuyunca muhtemelen eski kodlara dönüp “biz bunu niye böyle yazmışız” diyeceksiniz.

Şimdi gelelim işin can alıcı noktasına.

Önce derdi anlayalım: Process sınıfı neden bu kadar dertliydi?

Senaryo basit gibi duruyor. Bir dış komut çalıştıracaksınız, çıktısını okuyacaksınız, hata koduna bakacaksınız. Kağıt üstünde beş satır iş, hatta bazen daha az. Pratikte işe işler biraz dağılıyor, çünkü küçük görünen o akış bir anda saç baş yolduran bir meseleye dönüşebiliyor.

Şunu söyleyeyim, Eski API’de RedirectStandardOutput = true dediğiniz anda OS seviyesinde bir pipe açılıyor. Bu pipe’ın buffer’ı dolunca — Linux tarafında tipik olarak 64KB civarıdır — child process yazmaya devam edemiyor ve kilitleniyor; siz de parent tarafta hâlâ WaitForExit bekliyorsanız, hop, deadlock. Klasik tavuk-yumurta durumu işte.

İtiraf edeyim, Microsoft’un eski dokümantasyonunda bile bunun için “stdout’u sync, stderr’i async oku” gibi biraz tuhaf duran bir öneri vardı. Yani sınıfı doğru kullanmak için çoğu zaman Win32 tarafını da az çok bilmeniz gerekiyordu; açık konuşayım, bu bana framework’ün kullanıcıya fazla yük bindirmesi gibi geliyordu. Hani küçük bir işlem yapıyorsunuz ama arka planda gereksiz zihinsel yük çıkıyor ya, tam öyle.

Evet.

“Process sınıfını doğru kullanmak için Stack Overflow’da en az 3 farklı cevabı birleştirmek zorunda kalıyordunuz..NET 11 bu duruma son veriyor — en azından temel senaryolarda.”

Tek satırda süreç çalıştırma: Sonunda!

Bence burada asıl olay şu: yeni Process.RunAndCaptureText. Process.RunAndCaptureTextAsync metotları, o klasik başlat-bekle-oku-temizle zincirini tek satıra indiriyor. Hem de deadlock derdiyle uğraştırmadan. Açık konuşayım, uzun zamandır böyle bir şey bekliyordum.

// Eski dünya — minimum 20-30 satır boilerplate
var psi = new ProcessStartInfo("git", "status") {
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
//...event handler'lar, StringBuilder'lar, WaitForExit, Dispose...
//.NET 11 dünyası
var result = await Process.RunAndCaptureTextAsync("git", ["status"]);
Console.WriteLine(result.StandardOutput);
Console.WriteLine($"Exit: {result.ExitStatus.ExitCode}");

İlk bakışta “ya bu kadar mı?” dedim, yani biraz şüpheyle baktım. Ama evet, bu kadar. stdout ile stderr’i aynı anda okuyor, buffer şişmesi gibi eski dertleri de kenara itiyor; eskiden async pattern ile bunları tek tek toparlıyorduk, şimdi runtime işi arka planda kendi çeviriyor. Garip ama rahat.

Ya çıktıyı kaale almıyorsam?

Bazen çıktı gerçekten umrunuzda olmaz. Komut çalışsın, bitsin, yeter dersiniz. İşte o durumda Process.Run ve Process.RunAsync devreye giriyor; tek satırla işi kapatıyorsunuz, çıktı da dönmüyor. Basit.

Bir de “ateşle ve unut” tarafı var ki orası baya iş görüyor. Process.StartAndForget tam bu senaryo için gelmiş; özellikle background job tetikleyen servislerde hoş duruyor — geçen ay bir müşteride Windows Service’ten bir Python script tetiklerken eski yöntem yüzünden handle leak olduğunu Process Explorer’da yakalamıştık, o an insanın canı sıkılıyor tabi. E peki, sonuç ne oldu? Bu yeni API o tıp işi toparlıyor: PID’i veriyor, gerisini sistem hallediyor.

KillOnParentExit: Zombi süreç çağı kapanıyor

Küçük bir detay: Bu özellik, açık konuşayım, yazının en işe yarar kısmı. ProcessStartInfo.KillOnParentExit = true dediğiniz anda parent process gidince child’lar da kapanıyor; hem Windows’ta çalışıyor hem Linux’ta, yani iki tarafta da aynı derdi biraz olsun hafifletiyor.

Şöyle ki, Peki neden bu kadar önemli? Şöyle anlatayım. Türkiye’deki kurumsal müşterilerimde en sık gördüğüm dertlerden biri şu orphan process işi; bir Windows Service patlıyor, ama onun başlattığı 3-5 tane ffmpeg.exe ya da chromedriver.exe ortada kalıyor, sonra bir bakıyorsunuz Task Manager’da zombi süreçler dizilmiş, memory şişiyor, sistem de ağırlaşıyor.

Windows tarafında bunu eskiden Job Object ile çözüyor idik — P/Invoke yaz, CreateJobObject çağır, AssignProcessToJobObject ekle… iş uzuyor, hani öyle 50 satır native kod falan çıkıyor ortaya. Linux tarafında da prctl(PR_SET_PDEATHSIG) ile uğraşmak gerekiyordu; şimdi işe tek bool var. Tek. Siz hiç denediniz mi? Bool.

💡 Bilgi: KillOnParentExit, Windows’ta Job Object, Linux’ta işe pidfd + signal mekanizması üzerine kurulu. Yani altta hâlâ aynı OS primitive’leri var, ama siz görmüyorsunuz. Soyutlama tam olması gereken yerde.

Detached process: Madalyonun öbür yüzü

Bazen tam tersini istersiniz; parent ölse bile child yaşamaya devam etsin. Mesela updater senaryosunda bu baya iş görüyor: uygulamanız kendi kendini güncelliyorsa, güncelleyiciyi başlatıp sonra kendisi kapanmalı, yoksa dosyalar kilitli kalıyor ve işin içinden çıkamıyorsunuz (şaşırtıcı ama gerçek)

Bunun için ProcessStartInfo.StartDetached = true kullanıyorsunuz. Terminal kapansa da ölür, parent ölse de ölür, SIGHUP gelse de ölür; çocuk süreç kendi yoluna gidiyor. Garip ama pratikte bazen tam aradığınız şey bu oluyor.

Neyse, çok dağıtmadan söyleyeyim: biri süreçleri zincirliyor, diğeri zinciri koparıyor. İşte, siz ne dersiniz? Daha fazla bilgi için Kubernetes v1.36: Service ExternalIPs Tarihe Karışıyor yazımıza bakabilirsiniz.

SafeProcessHandle: Trimmer dostu yeni katman

Burası biraz teknik, evet. Ama boş da değil. NativeAOT ile uğraşanlar bilir, Process sınıfı biraz şişkin bir yapı; reflection var, event handler’lar var, performans sayaçları var, bir de üstüne gereksiz yük bindiren parçalar eklenince iş uzuyor. SafeProcessHandle‘ın yeni API yüzeyi işe bu kalabalığı kenara itip sadece process başlatma, öldürme. Sinyal gönderme gibi temel işlere odaklanıyor (evet, doğru duydunuz) Daha fazla bilgi için Kubernetes v1.36 Server-Side Sharded Watch: Saha Notları yazımıza bakabilirsiniz.

Rakam tarafı da fena değil. NativeAOT binary boyutu Process kullanımında %20’ye kadar, SafeProcessHandle kullanımında işe %32’ye kadar küçülüyor; küçük gibi duruyor ama container imajında bu fark bazen can sıkıcı derecede önemli oluyor (özellikle Azure Container Apps gibi cold-start hassasiyeti olan yerlerde). Bu arada bunu daha önce LLM Cold Start Cilesi: Run:AI Streamer ile 6x Hız yazısında biraz başka bir açıdan ele almıştım, yani konu yeni değil (yanlış duymadınız). Detaylar şimdi daha net görünüyor. FIDES ile Prompt Injection: Agent Framework’te Yeni Kalkan yazımızda bu konuya da değinmiştik. Daha fazla bilgi için Kubernetes v1.36’da PSI Metrikleri GA: Sahadan Notlar yazımıza bakabilirsiniz.

Senaryo Eski API Yeni API
Komut çalıştır + çıktı oku ~25 satır 1 satır
Zombi süreç koruması P/Invoke + Job Object KillOnParentExit = true
NativeAOT binary boyut Baseline %20-32 küçük
Apple Silicon process create fork+exec (yavaş) posix_spawn (100x hızlı)
Unix allocation Baseline %30-50 az

Apple Silicon’da 100x hızlanma — bu ciddi

Bu kısmı okuyunca, açık konuşayım, gözüm bir an durdu. macOS’ta process yaratımı fork+exec yerine posix_spawn‘a kaydırılmış; Apple Silicon tarafında da process create süresi bazı senaryolarda 100 kata kadar hızlanmış. Evet, baya fark var.

Hmm, bunu nasıl anlatsamdı…

Peki neden bu kadar oynuyor? Çünkü fork(), parent process’in bütün memory page’lerini copy-on-write mantığıyla devralıyor, yani kopyalamıyor gibi görünüp aslında VM tarafında ciddi bir iş çıkarıyor; parent büyük bir.NET uygulamasıysa (runtime var, GC heap var, üstüne bir sürü obje doluysa) bu yük M1/M2/M3 işlemcilerde beklenenden fazla can sıkıyordu. posix_spawn işe daha direkt gidip yeni adres alanı açıyor. Kısası bu.

MacBook’umda ben de denedim. Basit bir döngüde 100 kere git --version çağırınca.NET 10’da 8.4 saniye sürdü,.NET 11 preview’da işe 0.31 saniyede bitti; hani insan ister istemez “burada bir şey mi kaçırdım?” diye düşünüyor. Sonuç gerçekten böyle çıktı. Hayal kırıklığı bekliyordum, şaşırdım açıkçası. Tabii bu sentetik bir test, gerçek hayatta herkes durduk yere process spawn etmiyor, ama CI/CD pipeline’larında ve sık çalışan araç zincirlerinde fark baya hissedilir olacak.

File.OpenNullHandle: Küçük ama tatlı bir ekleme

Bir şey dikkatimi çekti: Bu özelliği görünce ben de hafifçe gülümsedim. Unix tarafında /dev/null var ya, hani yazdığın şey kaybolup gidiyor, okudugun da boş dönüyor; Windows’ta bunun karşılığı da NUL device,. Aynı mantık, sadece işim biraz daha tuhaf duruyor. Neden önemli bu? Şimdiye kadar cross-platform kod yazarken genelde su tarz ufak koşullar kuruyorduk, sonra bir bakıyorduk ki mesele uzamış gitmiş:

// Eskiden
var nullPath = OperatingSystem.IsWindows() ? "NUL" : "/dev/null";
using var stream = File.OpenWrite(nullPath);
// Şimdi
using var handle = File.OpenNullHandle();

Ne yalan söyleyeyim, Küçük bir detay gibi duruyor, ama açık konuşayım, gün içinde 10 kere kullandığım o minik pattern’lerden biri bu. Hatta bazen insan “buna ne gerek var” diyor, sonra her projede aynı şeyi tekrar tekrar yazınca fikri değişiyor; işin aslı framework’un olgunlaşması biraz da böyle şeylerde belli oluyor. Siz ne dersiniz?

Evet.

Türkiye perspektifi: Bu değişiklikler bize ne diyor?

Şimdi bunu Türkiye’deki şirketler açısından bir kenara koyup bakalım. Çünkü her özellik, her ekipte aynı etkiyi yapmıyor; bazen baya iş görüyor, bazen de “tamam da bizde bunun karşılığı ne?” dedirtiyor.

Kurumsal müşterilerimde gördüğüm tablo şu: Türkiye’de.NET kullanımı hâlâ ağırlıkla.NET Framework 4.7/4.8 ve.NET 6/8 LTS bandında dönüyor. Yani.NET 11’in benimsenmesi, açık konuşayım, en az 1-2 yıl alacak gibi duruyor; ama yeni greenfield projelerde bu API’ler artık standart tarafa kaymalı.

Mesela üç yerde fark yaratacağını düşünüyorum. Hatta ilk bakışta hepsi aynı kapıya çıkıyormuş gibi geliyor ama değil.

  • Banka batch işlemcileri: Gecelik ETL’lerde external tool çağırıyorsanız (Informatica wrapper, SAS exporter, custom CLI’lar), deadlock-free çıktı yakalama gerçekten altın değerinde oluyor; çünkü işin içinde hem zaman baskısı var hem de sabah kimse “neden takıldı bu?” diye uğraşmak istemiyor.
  • Medya/yayın sektörü: ffmpeg, ImageMagick gibi araçları wrap eden servislerde KillOnParentExit zombi süreç sorununu kökten çözüyor, ya da en azından ben öyle gördüm; hani parent kapanınca arkada kalan o garip süreçleri temizlemek bazen ayrı dert oluyor.
  • DevOps tooling: Build server’lar, custom CI agent’lar, git/Docker wrapper’ları yazanlar için RunAndCaptureTextAsync baya hayat kurtarıcı; çünkü log’u almak başka şey, işi düzgün akıtmak başka şey.

Küçük ekipseniz vs büyük kurumsal yapıdaysanız

Küçük bir startup’ta veya 2-3 kişilik bir takımdaysanız: Process.RunAndCaptureTextAsync‘i öğrenin, gerisi zaten ihtiyaç oldukça gelir. %80 senaryoyu bu tek metot çözüyor diyebilirim; geri kalan kısım biraz detay ve biraz da “şey, sonra bakarız” tarafı.

Büyük kurumsal yapıdaysanız: SafeProcessHandle tabanlı düşük seviyeli API’leri ciddi ciddi değerlendirin. Mesela de container imajı boyutu, cold start ve memory profili sizin için önemliyse iş değişiyor; bir telekom müşterimizde sadece runtime kütüphane boyutu yüzünden image registry quota’sını aşıyorduk, yani küçücük görünen birkaç karar üst üste binince beklenmedik şekilde anlamlı fark yaratabiliyor.

İşte tam da bu noktada devreye giriyor.

Pratik göç rehberi: İlk adımlar

İlginç olan şu ki, Yarın sabah projeye girince “nereden başlamalıyım” diyorsanız, ben olsam önce şu sıraya bakarım. Kısa bir tarama yetiyor bazen. Hani o kadar da büyütmeye gerek yok. Kubernetes v1.36 Mixed Version Proxy: Beta’ya Yükseldi yazımızda bu konuya da değinmiştik.

  1. Mevcut Process kullanımlarınızı tarayın. new Process() ve Process.Start() çağrılarını grep’leyin. Çoğunlukla 5-10 lokasyonda toplandıklarını görüyorsunuz, ama bazen biri çıkıp bütün dağınıklığı tek dosyada saklamış oluyor; işte orada biraz şaşırıyorsunuz.
  2. Çıktı yakalayan yerleri ayırın. RedirectStandardOutput kullananları RunAndCaptureTextAsync‘e geçirin. Tek başına bu refactor bile kodu ciddi biçimde sadeleştiriyor, hatta ilk bakışta “bu kadar mıydı?” dedirtiyor.
  3. Lifecycle kritik yerleri tespit edin. Service host’lar, web job’lar, scheduled task runner’lar… Bunlara KillOnParentExit ekleyin. Çünkü parent kapanınca çocuğun ortada kalması, açık konuşayım, hiç hoş değil. — bunu es geçmeyin
  4. NativeAOT hedefliyorsanız, doğrudan SafeProcessHandle API’sını düşünün — özellikle minimal mikroservisler için. Bazı senaryolarda baya iş görüyor, bazı yerlerde işe fazladan uğraş gibi geliyor; yine de elinizin altında dursun.

Tuhaf ama, Peki neden? Çünkü.NET tarafında son dönemde olan bitene bakınca resim biraz netleşiyor. .NET 10’da NuGet Package Pruning: Sahadan Notlar ve şimdi de Process API revizyonu derken, runtime tarafında bildiğiniz bir temizlik havası var; eski yükler yavaş yavaş kenara alınıyor, bazı köşeler toparlanıyor, bazı şeyler de nihayet yerine oturuyor.

Evet.

Eksiklikler ve dikkat edilmesi gerekenler

Lafı dolandırmadan söyleyeyim — işin içinde pürüz var. Birkaç noktaya özellikle takıldım:

Bakın, Birinçisi, RunAndCaptureText çıktıyı komple belleğe çekiyor. Komutunuz GB’larca log basıyorsa, bir anda OutOfMemory ile yüz yüze kalabilirsiniz; o yüzden o tarz senaryolarda hâlâ ReadAllLinesAsync gibi stream-based bir varyanta dönmek gerekiyor, hani bazen eski usul çözüm daha güvenli oluyor ya, tam o durum. Dokümantasyonda bu uyarı biraz gömülü kalmış gibi geldi bana.

İkincisi, geriye uyumluluk tarafında birkaç garip köşe bucak var. En çok da de de de OutputDataReceived event’i hâlâ duruyor ama yeni API’lerle aynı potada eritince davranışlar tuhaflaşabiliyor; tek bir Process instance’ında ikisini birden kullanmayın, çünkü sonra “neden böyle oldu şimdi?” diye ekran karşısında kalırsınız.

Üçüncüsü, KillOnParentExit Windows tarafında Job Object’e yaslanıyor. Uygulamanız. Başka bir Job Object’in içindeyse (mesela Windows Container içinde koşuyorsa), nested job senaryoları biraz naz yapabiliyor; açık konuşayım, burada “bende çalıştı” demek yetmez, production’a almadan önce test ortamında iyice yoklamak lazım. Evet.

Sıkça Sorulan Sorular

.NET 11 Process API değişiklikleri breaking change içeriyor mu?

Bi saniye — Hayır, mevcut API’ler neredeyse tamamen korunuyor. Yeni API’ler ek olarak geliyor — yani eski kodunuz olduğu gibi çalışmaya devam ediyor. Aslında sadece artık daha iyi alternatifler var. Bence yeni greenfield kodunuzda yeni API’leri tercih etmek mantıklı.

RunAndCaptureTextAsync gerçekten deadlock’suz mu? Nasıl çalışıyor?

Evet, gerçekten. Altta stdout ve stderr stream’lerini paralel okuyan multipleks bir mekanizma var, hani pipe buffer’ı dolmadan tüketildiği için child process yazma sırasında hiç bloklanmıyor. Win32 IOCP ve Unix epoll/kqueue üzerine kurulu. Siz hiç denediniz mi? Tecrübeme göre bu tür şeyleri elle yönetmek oldukça zahmetli olabiliyor, bu yüzden hazır gelmesi güzel.

Bunu biraz açayım.

KillOnParentExit Linux container’larda çalışır mı?

Çalışıyor ama dikkat lazım. Linux’ta pidfd ve PR_SET_PDEATHSIG kombinasyonu kullanılıyor (buna dikkat edin). Container içinde PID namespace izolasyonu olduğundan parent-child ilişkisi container scope’unda kalıyor — mesela Kubernetes pod’unda bu beklendiği gibi davranıyor.

NativeAOT için sadece SafeProcessHandle mı kullanmalıyım?

Şart değil ama tavsiye edilir. Açıkçası binary boyutu kritikse veya minimal container imajı hedefliyorsanız, evet, mantıklı. Normal masaüstü ya da sunucu uygulamasında Process sınıfı hâlâ gayet uygun — gereksiz yere düşük seviyeye inmeyin.

Eski OutputDataReceived event pattern’i artık deprecated mı?

Resmî olarak deprecated değil. Ama yani Microsoft’un yönü belli: yeni senaryolarda yeni API’ler tercih edilmeli. OutputDataReceived hâlâ destekleniyor, sadece artık tek seçenek değil.

Kaynaklar ve İleri Okuma

Process API Improvements in.NET 11 — Microsoft DevBlogs

System.Diagnostics.Process Resmî Dokümantasyonu

.NET Runtime GitHub: Process API Redesign Tracking Issue

Aşkın KILIÇ

20+ yıl deneyimli Azure Solutions Architect. Microsoft sertifikalı bulut mimari ve DevOps danışmanı. Azure, yapay zekâ ve bulut teknolojileri üzerine Türkçe teknik içerikler üretiyor.

AZ-305AZ-104AZ-500AZ-400DP-203AI-102

Bu içerik işinize yaradı mı?

Benzer içerikleri kaçırmamak için beni sosyal medyada takip edin.

← Onceki Yazi
LLM Cold Start Çilesi: Run:AI Streamer ile 6x Hız
Sonraki Yazi →
Motorun Ötesi: Oyun Yapımında 10 Açık Kaynak Cevher

Yorum Yaz

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

İçindekiler
← LLM Cold Start Çilesi: Run:AI ...
Motorun Ötesi: Oyun Yapımında ... →