Episode ini membahas concurrency di C#: Task-based async pattern dengan async/await, Parallel LINQ dan Task Parallel Library, Channel dan ValueTask dengan asynchronous streams, serta synchronization primitives dan thread safety untuk kode paralel yang benar.

Modern hardware punya banyak inti, dan aplikasi harus memanfaatkannya. Tapi menulis kode yang berjalan paralel itu sulit: race condition, deadlock, dan bug yang hanya muncul sekali-sekali adalah musuh terbesar developer.
Di episode 7 kalian sudah melihat async/await untuk I/O. Episode 14 memperluas cakupannya: kapan memakai async untuk I/O, kapan memakai paralelisme untuk CPU, bagaimana memproses aliran data yang tak berujung, dan bagaimana mengamankan data yang dibagi antar thread.
Task merepresentasikan operasi yang sedang berlangsung. async dan await membuat kode asynchronous mudah dibaca seperti kode sinkron:
async Task<string> MuatDataAsync(HttpClient client)
{
var request = new HttpRequestMessage(HttpMethod.Get,
"https://api.contoh.id/orders");
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
return body;
}
var hasil = await MuatDataAsync(client);
Console.WriteLine(hasil);Poin kunci: await tidak memblokir thread — saat menunggu, thread kembali ke pool dan bisa melayani pekerjaan lain. Untuk menjalankan beberapa operasi sekaligus, gunakan Task.WhenAll:
var t1 = MuatDataAsync(client);
var t2 = MuatDataAsync(client);
await Task.WhenAll(t1, t2);Task.WhenAll menunggu semua task selesai secara bersamaan, memangkas total waktu menjadi yang terlama, bukan jumlahnya.
Untuk komputasi berat yang murni CPU, PLINQ memparalelkan query LINQ hanya dengan .AsParallel():
var hasil = dataBesar
.AsParallel()
.Where(item => item.Berat > 100)
.Select(item => item.Proses())
.ToArray();
Console.WriteLine($"Hasil: {hasil.Length}");.AsParallel() mempartisi data ke beberapa thread. Gunakan hanya untuk operasi CPU-bound yang benar-benar berat — untuk pekerjaan kecil, overhead partisi justru memperlambat.
TPL menyediakan kontrol level rendah. Parallel.For menjalankan loop dengan banyak thread:
Parallel.For(0, 100, i =>
{
var kuadrat = i * i;
Console.WriteLine($"{i} kuadrat = {kuadrat}");
});Parallel.For membagi iterasi ke worker threads. Hati-hati: jangan panggil method yang tidak thread-safe di dalamnya, dan jangan mutasi state bersama tanpa sinkronisasi.
Channel adalah antrean aman untuk pola producer-consumer — satu kode menghasilkan data, kode lain mengonsumsinya:
var channel = Channel.CreateUnbounded<int>();
async Task Producer()
{
for (var i = 0; i < 10; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
}
async Task Consumer()
{
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"Terima {item}");
}
}
await Task.WhenAll(Producer(), Consumer());await foreach membaca aliran data tanpa memblokir. Pola ini menjadi fondasi pipeline pemrosesan yang scalable.
Meskipun System.Threading.Channels sudah menjadi bagian dari shared framework .NET modern, untuk project yang menargetkan framework lama atau ingin memakai versi terbaru, kalian bisa menambahkan paketnya dengan dotnet add package System.Threading.Channels.
Untuk operasi yang hampir selalu selesai sinkron, ValueTask menghindari alokasi objek Task:
ValueTask<int> BacaCepatAsync() => new(42);
async IAsyncEnumerable<int> AngkaBermain()
{
for (var i = 0; i < 5; i++)
{
await Task.Delay(50);
yield return i;
}
}
await foreach (var n in AngkaBermain())
{
Console.WriteLine(n);
}Method IAsyncEnumerable mengalirkan hasil bertahap — memori tidak membengkak meski data sangat banyak.
Saat banyak thread mengubah data yang sama, hasilnya tidak terduga tanpa sinkronisasi. lock memastikan hanya satu thread masuk ke blok kritis pada satu waktu:
class Counter
{
private int _nilai;
private readonly object _pintu = new();
public void Tambah()
{
lock (_pintu)
{
_nilai++;
}
}
public int Nilai => _nilai;
}Untuk operasi atomik sederhana, Interlocked lebih cepat daripada lock:
Interlocked.Increment(ref _nilai);Aturan praktis thread safety: kurangi data bersama, utamakan immutable data, dan sinkronkan hanya saat benar-benar perlu. SemaphoreSlim untuk membatasi jumlah thread bersamaan, dan ConcurrentDictionary serta ConcurrentQueue untuk koleksi yang aman dari banyak thread.
Inti yang harus dibawa pulang:
await tidak memblokir thread; Task.WhenAll menjalankan banyak task sekaligus.Parallel.For untuk beban CPU; async untuk beban I/O.ValueTask menghindari alokasi untuk operasi yang sering selesai sinkron.lock, Interlocked, dan koleksi concurrent menjaga thread safety.Di episode 15 selanjutnya kita mengejar kecepatan: performance tuning dan diagnostics — profiling dengan dotnet-trace dan dotnet-counters, memory diagnostics dan garbage collection, optimasi dengan Span, Memory, dan stackalloc, serta JIT optimization dan tiered compilation.