Bir Task, devam eden işi temsil eden bir nesnedir. Bir thread pool thread'inde çalışıyor, bir timer'ı ya da bir ağ cevabını bekliyor olabilir; her durumda bitip bitmediğini sorabilir, sonucunu alabilir, onu bekleyebilir ya da ona daha fazla iş ekleyebilirsiniz. async ve await, task'lar üzerindeki dil sözdizimidir. Bu sayfa Task API'sinin kendisini ve thread'leri ele alır.
Task.Run: thread pool'da iş
Task.Run bir lambda alır ve onu thread pool'a kuyruklar; thread pool, .NET'in her iş için yeni bir thread oluşturmamak için hazırda tuttuğu bir thread kümesidir. Bir Task ya da lambda bir değer döndürüyorsa bir Task<T> döndürür.
Çıktı:
Started, completed yet: False
Primes up to 200,000: 17984
Status: RanToCompletion
Result, task bitene kadar çağıran thread'i bloklar. Bir konsol programının Main'inde bu kabul edilebilir, ama async kodun içinde bloklamadan bekleyen int primes = await task; yazardınız. Aynısı sonucu olmayan bir Task için task.Wait() için de geçerlidir.
Task.Run CPU'ya bağlı iş içindir: bir çekirdeği meşgul tutan hesaplamalar. Bir dosya okumak ya da bir web API'si çağırmak gibi G/Ç için kütüphanenin zaten sağladığı async metodu (File.ReadAllTextAsync, HttpClient.GetStringAsync) kullanın ve doğrudan await edin. G/Ç beklerken await edilen bir async çağrı hiçbir thread'i tutmaz; bu yüzden onu Task.Run içine sarmak bir şey kazandırmaz, yalnızca thread pool üzerinden fazladan bir geçiş ekler. Bir pool thread'ini gerçekten meşgul eden durum, File.ReadAllText gibi senkron ve bloklayan bir çağrının Task.Run içine sarılmasıdır: o thread disk ya da ağ yanıt verene kadar boşta bekler.
Aynı anda birkaç task: WhenAll
Bağımsız işi task'lara bölün, hepsini başlatın ve sonuçları Task.WhenAll ile birleştirin. Birkaç çekirdekli bir makinede parçalar paralel çalışır.
Çıktı:
Parts: 14999995, 14999999, 15000003, 15000000
Total: 59999997
Her parça kendi aralığında çalışır ve kendi toplamını döndürür, bu yüzden task'lar hiçbir şey paylaşmaz ve lock gerektirmez. WhenAll, thread'ler hangi sırayla bitmiş olursa olsun sonuçları task'ların sırasıyla döndürür. Task'ların paylaşılan durumu güncellemesi gerektiğinde lock sayfasına bakın.
WhenAny ve zaman aşımları
Task.WhenAny, task'larından ilki tamamlandığında tamamlanır ve o task'ı döndürür. Gerçek işi Task.Delay ile yarıştırmak bir zaman aşımı verir:
Çıktı:
fast mirror answered after 100 ms
Timed out after 300 ms
WhenAny sonucunu değil kazanan task'ın kendisini döndürür, bu yüzden değeri (ya da başarısız olduysa istisnayı) almak için onu tekrar await edersiniz. Kaybeden task'ların çalışmaya devam ettiğine dikkat edin; WhenAny onları durdurmaz. Yavaş işi gerçekten durdurmak için aşağıda gösterildiği gibi bir token ile iptal edin. .NET 6 ve sonrasında await work.WaitAsync(TimeSpan.FromMilliseconds(300)) aynı zaman aşımını tek bir çağrıda ifade eder.
ContinueWith ile devamlar
await'ten önce, bir task bittikten sonra kod çalıştırmanın yolu ContinueWith'ti. Onu eski kodda hâlâ görürsünüz:
Çıktı:
Report total: 42
Her devam önceki task'ı alır ve o tamamlandığında çalışır. await aynı şeyi çok daha az törenle yapar, istisnaları doğal şekilde ele alır ve doğru bağlama döner, bu yüzden yeni kodda onu tercih edin. ContinueWith önceki task başarısız olsa ya da iptal edilse bile çalışır; bu yüzden eski kod, başarısız bir task üzerinde .Result çağırmaktan kaçınmak için t.IsFaulted'i incelemek ya da TaskContinuationOptions vermek zorundadır.
CancellationToken ile iptal
Task'lar iş birliğiyle iptal edilir. Bir CancellationTokenSource bir token verir; işi yapan kod token'ı kontrol eder ve kendini durdurur. Hiçbir şey zorla durdurulmaz.
Çıktı:
Cancelled before finishing all 10 orders
Processed 3
Token her katmandan aşağı geçirilir ve Task.Delay, HttpClient ile veritabanı çağrıları gibi async API'ler de onu kabul eder, böylece bir iptal onları beklemenin ortasında durdurur. OperationCanceledException'ı yakalayın; bazı API'lerin fırlattığı TaskCanceledException ondan türer. CancellationTokenSource IDisposable'dır, using bu yüzdendir.
Thread ile Task
Bir Thread, kendiniz oluşturduğunuz bir işletim sistemi thread'idir. Daha ağırdır: her birinin kendi stack'i vardır (Windows'ta varsayılan olarak 1 MB), birini başlatmak ölçülebilir zaman alır ve size hiçbir sonuç ya da istisna yönetimi vermez.
Çıktı:
Thread started
Result: 42
Thread | Task | |
|---|---|---|
| Çalıştığı yer | Yeni, ayrılmış bir OS thread'i | Bir pool thread'i (ya da G/Ç için hiç thread) |
| Başlatma maliyeti | Yüksek | Düşük |
| Sonuç değeri | Yok, bir değişken paylaşın | Task<T>.Result / await |
| İstisnalar | Ele alınmayanlar süreci çökertir | Task'ta saklanır, await'te yeniden fırlatılır |
| İptal | Elle bayraklar | Yerleşik CancellationToken |
| Onu beklemek | Join() | await, Wait(), WhenAll |
Thread'e yalnızca havuzun vermediği bir şeye ihtiyacınız olduğunda başvurun: asla bir pool thread'i ödünç almaması gereken uzun süreli bir döngü, belirli bir öncelik ya da COM için tek thread'li bir apartman. Uzun süreli task'lar için Task.Factory.StartNew(work, TaskCreationOptions.LongRunning) task API'sini korurken ayrılmış bir thread alır.
Parallel.For ve Parallel.ForEach
"Bunu her öğeye tüm çekirdekleri kullanarak yap" için System.Threading.Tasks.Parallel elle task oluşturmaktan daha basittir. Aralığı thread'ler arasında bölüştürür ve tüm yinelemeler bittiğinde döner:
Örnek çıktı:
image 4 on thread 7
image 3 on thread 6
image 5 on thread 8
image 0 on thread 1
image 1 on thread 4
image 2 on thread 5
Total pixels: 385425
Görüntü başına satırlar thread'ler onları hangi sırayla çalıştırdıysa o sırayla çıkar ve thread ID'leri çalıştırmadan çalıştırmaya değişir; yalnızca toplam sabittir. Paralel döngülerin doğası budur: yinelemeler birbirinin sırasına bağlı olmamalıdır ve paylaşılan her durum senkronizasyon gerektirir (burada Interlocked.Add). Parallel.ForEach herhangi bir koleksiyon üzerinde aynısını yapar ve .NET 6 async gövdeler için Parallel.ForEachAsync'i ekler.
Yaygın hatalar
- Async kodun içinde
.Resultya da.Wait(). Bir thread'i bloklar ve UI uygulamalarında deadlock'a yol açabilir; bunun yerineawaitkullanın. .Result'ın etrafında orijinal istisna tipini yakalamak. BaşarısızlıklarAggregateExceptioniçine sarılı gelir;awaitonları açar.- Ateşle ve unut. Await edilmeyen ya da saklanmayan
Task.Run(...)istisnalarını kaybeder. - G/Ç'nin etrafında
Task.Run. Bir async metodun etrafında yalnızca thread pool üzerinden fazladan bir geçiş ekler;File.ReadAllTextgibi bloklayan bir metodun etrafında ise bekleme boyunca bir pool thread'ini tutar. Async G/Ç metodunu doğrudan await edin. - Token'ı yok saymak. İş onu kontrol etmedikçe bir
CancellationTokengeçirmek hiçbir şey yapmaz. - Paralel kodda senkronize edilmemiş paylaşılan durum.
Interlocked,lockkullanın ya da her task'a kendi verisini verin.
Sıkça Sorulan Sorular
C#'ta Task nedir?
Bir Task, gelecekte bitebilecek bir işlemi temsil eder: thread pool'da kod çalıştırmak, bir timer, bir G/Ç isteği. Await edilebilir, tamamlanıp tamamlanmadığını, başarısız olup olmadığını ya da iptal edilip edilmediğini bildirir ve Task<T> ayrıca bir sonuç taşır. Task'lar async/await'in altındaki yapı taşıdır.
C#'ta Task ile Thread arasındaki fark nedir?
Bir Thread, kendi stack'i olan, sizin oluşturup yönettiğiniz bir işletim sistemi thread'idir; başlatması pahalıdır ve değer döndürmez. Bir Task ise işi tanımlar ve Task.Run onu yeniden kullanılabilir thread'lerden oluşan paylaşılan bir havuzda planlar; bir sonuç döndürebilir, istisnaları yayabilir, iptal edilebilir ve await edilebilir. Özel ayarlara sahip, uzun ömürlü, ayrılmış bir thread'e ihtiyacınız yoksa task'ları kullanın.
Task.Run ne zaman kullanılmalı?
Geçerli thread'in dışına almak istediğiniz CPU'ya bağlı iş için: görüntü işleme, büyük bir hesaplama, bellekteki büyük bir dosyayı parse etmek; özellikle bir arayüzü duyarlı tutmak için. G/Ç'yi Task.Run içine sarmayın: ReadAllTextAsync gibi async G/Ç metotları beklerken thread'i zaten serbest bırakır.
C#'ta bir Task nasıl iptal edilir?
Bir CancellationTokenSource oluşturun, onun Token'ını işe verin ve kaynak üzerinde Cancel() (ya da CancelAfter(timeout)) çağırın. İş iş birliği yapmalıdır: döngüsünde token.IsCancellationRequested'ı kontrol etmeli ya da token.ThrowIfCancellationRequested() çağırmalı ve token'ı Task.Delay gibi async API'lere iletmelidir. İptal edilmiş bir task'ı await etmek OperationCanceledException fırlatır.
Task.WhenAll ile Task.WhenAny arasındaki fark nedir?
Task.WhenAll her task tamamlandığında tamamlanır ve size tüm sonuçları verir. Task.WhenAny ilk task tamamlanır tamamlanmaz tamamlanır ve o task'ı döndürür. WhenAny bir zaman aşımı eklemenin olağan yoludur: işi Task.Delay ile yarıştırın.
Task.Result neden AggregateException fırlatıyor?
.Result ve .Wait() her başarısızlığı bir AggregateException içine sarar, çünkü bir task genel olarak birkaç istisna tutabilir. await onu açar ve ilk iç istisnayı doğrudan yeniden fırlatır; await'i tercih etmenin bir nedeni budur. Bloklamanız gerekiyorsa .GetAwaiter().GetResult() de orijinal istisnayı fırlatır.