Task adalah object yang mewakili pekerjaan yang sedang berlangsung. Pekerjaan itu mungkin sedang berjalan di thread dari thread pool, menunggu sebuah timer, atau menunggu balasan dari jaringan; apa pun kasusnya, kamu bisa menanyakan apakah ia sudah selesai, mengambil hasilnya, menunggunya, atau menempelkan pekerjaan lain padanya. async dan await adalah sintaks bahasa di atas task. Halaman ini membahas API Task itu sendiri, serta thread.
Task.Run: pekerjaan di thread pool
Task.Run menerima sebuah lambda dan memasukkannya ke antrean thread pool, kumpulan thread yang disimpan .NET agar tidak perlu membuat thread baru untuk setiap pekerjaan. Method ini mengembalikan Task, atau Task<T> jika lambda-nya mengembalikan nilai.
Output:
Started, completed yet: False
Primes up to 200,000: 17984
Status: RanToCompletion
Result memblokir thread pemanggil sampai task selesai. Itu bisa diterima di Main sebuah program console, tetapi di dalam kode async kamu akan menulis int primes = await task;, yang menunggu tanpa memblokir. Hal yang sama berlaku untuk task.Wait() pada Task yang tidak punya hasil.
Task.Run dipakai untuk pekerjaan CPU-bound: perhitungan yang membuat sebuah core terus sibuk. Untuk I/O, seperti membaca file atau memanggil web API, pakai method async yang sudah disediakan library (File.ReadAllTextAsync, HttpClient.GetStringAsync) lalu await langsung. Selama I/O masih berjalan, pemanggilan async yang ditunggu dengan await sama sekali tidak memakai thread, jadi membungkusnya dengan Task.Run tidak memberi keuntungan apa pun dan hanya menambah satu lompatan lewat thread pool. Kasus yang benar-benar menahan sebuah thread pool adalah Task.Run di sekitar pemanggilan synchronous yang memblokir seperti File.ReadAllText: thread itu menganggur sampai disk atau jaringan menjawab.
Beberapa task sekaligus: WhenAll
Pecah pekerjaan yang saling independen menjadi beberapa task, mulai semuanya, lalu gabungkan hasilnya dengan Task.WhenAll. Di komputer dengan beberapa core, potongan-potongan itu berjalan secara paralel.
Output:
Parts: 14999995, 14999999, 15000003, 15000000
Total: 59999997
Setiap potongan mengerjakan rentangnya sendiri dan mengembalikan jumlahnya sendiri, jadi task-task itu tidak berbagi apa pun dan tidak butuh locking. WhenAll mengembalikan hasilnya sesuai urutan task-nya, bagaimanapun urutan selesainya thread. Jika task memang perlu mengubah state bersama, lihat lock.
WhenAny dan timeout
Task.WhenAny selesai saat task pertama di antara task-task itu selesai, lalu mengembalikan task tersebut. Mengadu pekerjaan yang sebenarnya dengan Task.Delay menghasilkan sebuah timeout:
Output:
fast mirror answered after 100 ms
Timed out after 300 ms
WhenAny mengembalikan task pemenangnya itu sendiri, bukan hasilnya, jadi kamu perlu melakukan await lagi untuk mendapatkan nilainya (atau exception-nya, jika gagal). Perhatikan bahwa task yang kalah tetap berjalan; WhenAny tidak menghentikannya. Untuk benar-benar menghentikan pekerjaan yang lambat, batalkan dengan token seperti di bawah. Di .NET 6 dan sesudahnya, await work.WaitAsync(TimeSpan.FromMilliseconds(300)) mengungkapkan timeout yang sama dalam satu pemanggilan.
Continuation dengan ContinueWith
Sebelum ada await, cara menjalankan kode setelah sebuah task selesai adalah ContinueWith. Kamu masih akan menemuinya di kode lama:
Output:
Report total: 42
Setiap continuation menerima task sebelumnya dan berjalan saat task itu selesai. await melakukan hal yang sama dengan jauh lebih sedikit basa-basi, menangani exception secara alami, dan kembali ke context yang benar, jadi lebih baik pakai cara itu di kode baru. ContinueWith tetap berjalan meskipun task sebelumnya gagal atau dibatalkan, jadi kode lama harus memeriksa t.IsFaulted atau memberikan TaskContinuationOptions agar tidak memanggil .Result pada task yang gagal.
Pembatalan dengan CancellationToken
Task dibatalkan secara kooperatif. CancellationTokenSource mengeluarkan sebuah token; kode yang mengerjakan tugasnya memeriksa token itu dan menghentikan dirinya sendiri. Tidak ada yang dihentikan secara paksa.
Output:
Cancelled before finishing all 10 orders
Processed 3
Token itu diteruskan ke setiap lapisan, dan API async seperti Task.Delay, HttpClient, dan pemanggilan database juga menerimanya, jadi pembatalan bisa menghentikan mereka di tengah penantian. Tangkap OperationCanceledException; TaskCanceledException yang dilempar sebagian API diturunkan darinya. CancellationTokenSource adalah IDisposable, itulah sebabnya ada using.
Thread vs Task
Thread adalah thread sistem operasi yang kamu buat sendiri. Ia lebih berat: masing-masing punya stack sendiri (1 MB secara default di Windows), memulainya butuh waktu yang terasa, dan ia tidak memberimu hasil atau penanganan exception.
Output:
Thread started
Result: 42
Thread | Task | |
|---|---|---|
| Berjalan di | Thread OS baru yang khusus | Thread dari pool (atau tanpa thread, untuk I/O) |
| Biaya untuk memulai | Tinggi | Rendah |
| Nilai hasil | Tidak ada, pakai variabel bersama | Task<T>.Result / await |
| Exception | Yang tidak ditangani membuat proses crash | Disimpan di task, dilempar ulang saat await |
| Pembatalan | Flag manual | CancellationToken bawaan |
| Menunggunya | Join() | await, Wait(), WhenAll |
Pakai Thread hanya jika kamu butuh sesuatu yang tidak diberikan pool: perulangan berumur panjang yang tidak boleh meminjam thread dari pool, prioritas tertentu, atau single-threaded apartment untuk COM. Untuk task yang berumur panjang, Task.Factory.StartNew(work, TaskCreationOptions.LongRunning) mendapat thread khusus sambil tetap memakai API task.
Parallel.For dan Parallel.ForEach
Untuk "lakukan ini pada setiap item, dengan memakai semua core", System.Threading.Tasks.Parallel lebih sederhana daripada membuat task secara manual. Class ini membagi rentangnya ke beberapa thread dan baru kembali setelah semua iterasi selesai:
Contoh output:
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
Baris untuk setiap gambar muncul dalam urutan apa pun sesuai urutan thread menjalankannya, dan ID thread-nya berbeda di setiap kali dijalankan; hanya totalnya yang tetap. Begitulah sifat perulangan paralel: iterasinya tidak boleh bergantung pada urutan satu sama lain, dan state bersama apa pun butuh sinkronisasi (di sini Interlocked.Add). Parallel.ForEach melakukan hal yang sama pada collection apa pun, dan .NET 6 menambahkan Parallel.ForEachAsync untuk body yang async.
Kesalahan umum
.Resultatau.Wait()di dalam kode async. Ini memblokir thread dan bisa menyebabkan deadlock di aplikasi UI; pakaiawait.- Menangkap tipe exception asli di sekitar
.Result. Kegagalan datang dalam bungkusanAggregateException;awaitmembuka bungkusan itu. - Fire and forget.
Task.Run(...)tanpa await atau tanpa menyimpan task-nya akan menghilangkan exception-nya. Task.Rundi sekitar I/O. Di sekitar method async, ini hanya menambah lompatan lewat thread pool; di sekitar method yang memblokir sepertiFile.ReadAllText, ini menahan sebuah thread dari pool selama seluruh waktu tunggu. Await langsung method I/O yang async.- Mengabaikan token. Meneruskan
CancellationTokentidak berpengaruh apa pun kecuali pekerjaannya memeriksa token itu. - State bersama tanpa sinkronisasi di kode paralel. Pakai
Interlocked,lock, atau beri setiap task datanya sendiri.
Pertanyaan yang sering diajukan
Apa itu Task di C#?
Task mewakili sebuah operasi yang mungkin selesai di masa depan: kode yang berjalan di thread pool, sebuah timer, atau request I/O. Task bisa ditunggu dengan await, memberi tahu apakah ia selesai, gagal, atau dibatalkan, dan Task<T> juga membawa sebuah hasil. Task adalah fondasi di balik async/await.
Apa perbedaan Task dan Thread di C#?
Thread adalah thread sistem operasi yang kamu buat dan kelola sendiri, dengan stack-nya sendiri; memulainya mahal dan ia tidak mengembalikan nilai. Task menggambarkan sebuah pekerjaan, dan Task.Run menjadwalkannya di pool berisi thread yang dipakai ulang; task bisa mengembalikan hasil, meneruskan exception, dibatalkan, dan ditunggu dengan await. Pakai task, kecuali kamu butuh thread khusus yang berumur panjang dengan pengaturan tertentu.
Kapan sebaiknya memakai Task.Run?
Untuk pekerjaan CPU-bound yang ingin kamu pindahkan dari thread saat ini: pemrosesan gambar, perhitungan besar, parsing file besar di memori, terutama agar UI tetap responsif. Jangan membungkus I/O dengan Task.Run: method I/O async seperti ReadAllTextAsync sudah membebaskan thread selama menunggu.
Bagaimana cara membatalkan Task di C#?
Buat CancellationTokenSource, teruskan Token-nya ke pekerjaan itu, lalu panggil Cancel() (atau CancelAfter(timeout)) pada source-nya. Pekerjaannya harus ikut bekerja sama: periksa token.IsCancellationRequested atau panggil token.ThrowIfCancellationRequested() di dalam perulangannya, dan teruskan token itu ke API async seperti Task.Delay. Melakukan await pada task yang dibatalkan akan melempar OperationCanceledException.
Apa perbedaan Task.WhenAll dan Task.WhenAny?
Task.WhenAll selesai saat setiap task sudah selesai dan memberimu semua hasilnya. Task.WhenAny selesai begitu task pertama selesai dan mengembalikan task itu. WhenAny adalah cara biasa untuk menambahkan timeout: adu pekerjaannya dengan Task.Delay.
Kenapa Task.Result melempar AggregateException?
.Result dan .Wait() membungkus setiap kegagalan dalam AggregateException, karena secara umum sebuah task bisa menyimpan beberapa exception. await membuka bungkusan itu dan langsung melempar ulang exception pertama di dalamnya, dan ini salah satu alasan untuk lebih memilih await. Jika kamu terpaksa memblokir, .GetAwaiter().GetResult() juga melempar exception aslinya.