Menu

C# Task: Task.Run, WhenAll, WhenAny, İptal ve Thread'ler

Task, .NET'te asenkron ve paralel işin birimidir. CPU'ya bağlı iş için Task.Run'ı, sonuçları okumayı, task'ları WhenAll ve WhenAny ile birleştirmeyi, devamları, CancellationToken ile iptali, bir Task'ın bir Thread'den farkını ve Parallel.For'un ne zaman daha uygun olduğunu öğrenin.

Bu sayfada çalıştırılabilir editörler var - düzenle, çalıştır ve sonucu anında gör.

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
ThreadTask
Çalıştığı yerYeni, ayrılmış bir OS thread'iBir pool thread'i (ya da G/Ç için hiç thread)
Başlatma maliyetiYüksekDüşük
Sonuç değeriYok, bir değişken paylaşınTask<T>.Result / await
İstisnalarEle alınmayanlar süreci çökertirTask'ta saklanır, await'te yeniden fırlatılır
İptalElle bayraklarYerleşik CancellationToken
Onu beklemekJoin()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 .Result ya da .Wait(). Bir thread'i bloklar ve UI uygulamalarında deadlock'a yol açabilir; bunun yerine await kullanın.
  • .Result'ın etrafında orijinal istisna tipini yakalamak. Başarısızlıklar AggregateException içine sarılı gelir; await onları 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.ReadAllText gibi 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 CancellationToken geçirmek hiçbir şey yapmaz.
  • Paralel kodda senkronize edilmemiş paylaşılan durum. Interlocked, lock kullanı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.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA