This episode covers async/await, Task and ValueTask, cancellation tokens, parallel programming with Parallel.For and Channels, and thread safety and synchronization primitives. You will write responsive I/O code without blocking threads.

I/O operations — reading files, calling APIs, querying databases — take up the largest part of an application's execution time. Episode 14 teaches you how to run these operations without blocking threads: the async/await pattern, Task, cancellation, and parallel programming.
The importance cannot be understated: a web server that blocks a thread while waiting on a database will run out of threads quickly. With async, the thread is released to serve other requests while the I/O operation waits. This is the difference between a slow API and an API serving thousands of concurrent requests.
The async keyword marks a method that can be suspended, and await waits for an async operation without blocking the thread:
public async Task<List<Produk>> GetProdukAsync()
{
using var db = new AppDbContext();
return await db.Produk
.Where(p => p.Stok > 0)
.ToListAsync();
}await db.Produk...ToListAsync() releases the thread while the database query runs and resumes it when the results are available. ToListAsync is the async version of ToList. Methods that use await must be marked async and return Task or Task<T>.
When await runs, control returns to the caller and the thread is free to do other work. When the operation finishes, execution resumes — usually on the same thread pool. This is why async makes applications more scalable, not merely faster.
A Task is the representation of an operation in progress. For operations that frequently complete synchronously (for example, from a cache), ValueTask reduces allocations:
public ValueTask<Produk> GetCachedAsync(int id)
{
if (_cache.TryGetValue(id, out var produk))
{
return ValueTask.FromResult(produk);
}
return LoadFromDbAsync(id);
}ValueTask.FromResult returns a value directly without allocating a Task. Use ValueTask only when profiling shows allocation pressure; Task remains the safe default.
Long-running operations must be cancellable. A CancellationToken propagates the cancellation signal:
public async Task<string> CallApiAsync(CancellationToken token)
{
using var http = new HttpClient();
return await http.GetStringAsync("https://api.example.com", token);
}The CancellationToken is passed to HttpClient and the database. When a client disconnects, the framework signals the token and the operation stops early — saving server resources. Always accept and forward the CancellationToken from the parameter all the way to the I/O operation.
For heavy independent CPU work, Parallel.For splits the work across many threads:
var hasil = new int[1_000_000];
Parallel.For(0, hasil.Length, i =>
{
hasil[i] = Compute(i);
});Parallel.For(0, hasil.Length, ...) executes the lambda on many threads at once. It suits large, independent data transformations. Don't use it for I/O — for that, async is the better choice.
A Channel implements a thread-safe queue for the producer-consumer pattern:
var channel = Channel.CreateUnbounded<string>();
await channel.Writer.WriteAsync("pesan 1");
await channel.Reader.ReadAsync();Channel.CreateUnbounded<string>() creates a queue; Writer puts items in and Reader takes them out. This pattern is the basis of processing pipelines — many workers read from a single channel and process items in parallel.
When many threads write the same data, results can be inconsistent. lock restricts access to a single thread:
private readonly object _lock = new();
private int _penghitung;
public void Tambah()
{
lock (_lock)
{
_penghitung++;
}
}lock (_lock) ensures only one thread enters the block at a time. For concurrent collections, consider ConcurrentDictionary and ConcurrentQueue, which are designed to be thread-safe from the start — faster and less error-prone than manual locks.
A guide:
Warning
Don't take a lock inside async code — lock blocks the thread. Use SemaphoreSlim with await WaitAsync() for synchronization in an asynchronous context.
Key takeaways:
In the next episode 15 we will discuss performance and diagnostics — profiling with dotnet-trace and dotnet-counters, memory diagnostics and GC tuning, Span and Memory for minimal allocations, and hot reload, tiered compilation, and AOT considerations.