Mục tiêu: Viết code hiệu năng cao, hiểu async internals, và sử dụng diagnostic tools chuyên nghiệp.
1. Benchmarking với BenchmarkDotNet
csharp// Cài: dotnet add package BenchmarkDotNet
[MemoryDiagnoser] // Đo memory allocation
[RankColumn] // Xếp hạng
[SimpleJob(RuntimeMoniker.Net90)]
public class StringConcatBenchmarks
{
private readonly string[] _items = Enumerable.Range(0, 1000)
.Select(i => $"item-{i}")
.ToArray();
[Benchmark(Baseline = true)]
public string StringConcat()
{
var result = "";
foreach (var item in _items)
result += item + ",";
return result;
}
[Benchmark]
public string StringBuilder_()
{
var sb = new StringBuilder();
foreach (var item in _items)
sb.Append(item).Append(',');
return sb.ToString();
}
[Benchmark]
public string StringJoin() => string.Join(",", _items);
[Benchmark]
public string ZeroAllocSpan()
{
// Dùng ArrayPool + Span cho zero-alloc
var buffer = ArrayPool<char>.Shared.Rent(16384);
try
{
var span = buffer.AsSpan();
int pos = 0;
foreach (var item in _items)
{
item.AsSpan().CopyTo(span[pos..]);
pos += item.Length;
span[pos++] = ',';
}
return new string(span[..pos]);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
}
// Chạy: dotnet run -c Release
// Kết quả mẫu:
// | Method | Mean | Allocated |
// |--------------- |------------ |---------- |
// | StringConcat | 1,234.56 μs | 2.1 MB | ← Tệ nhất
// | StringBuilder_ | 45.23 μs | 16 KB |
// | StringJoin | 38.12 μs | 12 KB |
// | ZeroAllocSpan | 32.45 μs | 8 KB | ← Tốt nhất
2. Object Pooling
ObjectPool<T>
csharp// Microsoft.Extensions.ObjectPool (built-in ASP.NET Core)
// Dùng cho objects tốn kém khi create/destroy
public class PdfGenerator
{
private static readonly ObjectPool<StringBuilder> _sbPool =
new DefaultObjectPoolProvider()
.CreateStringBuilderPool(initialCapacity: 1024, maximumRetainedCapacity: 16384);
public string GenerateReport(IEnumerable<ReportRow> rows)
{
var sb = _sbPool.Get();
try
{
sb.AppendLine("<html><body><table>");
foreach (var row in rows)
{
sb.Append("<tr><td>").Append(row.Name).Append("</td>");
sb.Append("<td>").Append(row.Value).Append("</td></tr>");
}
sb.AppendLine("</table></body></html>");
return sb.ToString();
}
finally
{
_sbPool.Return(sb); // Reset & return to pool
}
}
}
// Custom object pool
public class ExpensiveResourcePolicy : IPooledObjectPolicy<ExpensiveResource>
{
public ExpensiveResource Create() => new ExpensiveResource();
public bool Return(ExpensiveResource obj)
{
if (obj.IsCorrupted) return false; // Don't return broken objects
obj.Reset();
return true;
}
}
builder.Services.AddSingleton<ObjectPool<ExpensiveResource>>(sp =>
{
var provider = sp.GetRequiredService<ObjectPoolProvider>();
return provider.Create(new ExpensiveResourcePolicy());
});
ArrayPool<T>
csharp// Tránh allocate large arrays lặp lại
public class ImageProcessor
{
public async Task<byte[]> ResizeAsync(Stream imageStream, int targetSize)
{
// Rent buffer - có thể lớn hơn requested size!
var buffer = ArrayPool<byte>.Shared.Rent(targetSize);
try
{
var bytesRead = await imageStream.ReadAsync(buffer.AsMemory(0, targetSize));
// Process image...
return buffer[..bytesRead].ToArray(); // Copy result
}
finally
{
ArrayPool<byte>.Shared.Return(buffer, clearArray: true); // Clear sensitive data
}
}
}
// ⚠️ Lưu ý ArrayPool:
// 1. Kích thước trả về có thể LỚN HƠN kích thước yêu cầu
// 2. PHẢI return sau khi dùng xong (memory leak nếu không)
// 3. clearArray: true nếu buffer chứa sensitive data
3. Async Deep Dive
ValueTask vs Task
csharp// Task: luôn allocate object trên heap
// ValueTask: stack-allocated nếu result đã sẵn sàng
// ✅ Dùng ValueTask khi hot path có thể return ngay (cached result)
public ValueTask<Product?> GetProductAsync(int id)
{
// Cache hit → return ngay, zero allocation
if (_cache.TryGetValue(id, out var product))
return ValueTask.FromResult<Product?>(product);
// Cache miss → async operation
return LoadFromDatabaseAsync(id);
}
// ⚠️ ValueTask restrictions:
// 1. KHÔNG await 2 lần
// 2. KHÔNG dùng Task.WhenAll(valueTask1, valueTask2)
// 3. KHÔNG lưu vào biến rồi await sau
// ❌ Dangerous
var vt = GetProductAsync(1);
var result1 = await vt;
var result2 = await vt; // 💥 Undefined behavior!
// ✅ Safe: convert to Task nếu cần
var vt = GetProductAsync(1);
var task = vt.AsTask(); // Convert 1 lần
await task; // OK
ConfigureAwait Deep Dive
csharp// ConfigureAwait(false): KHÔNG capture SynchronizationContext
// → Không switch về UI thread / ASP.NET request thread sau await
// ASP.NET Core: KHÔNG CÓ SynchronizationContext → không cần ConfigureAwait(false)
// Nhưng: Library code NÊN luôn dùng ConfigureAwait(false)
// Vì library có thể được gọi từ WPF/WinForms/ASP.NET Framework
// Library code pattern:
public class MyLibrary
{
public async Task<T> ProcessAsync<T>(Func<Stream, Task<T>> processor)
{
using var stream = await OpenStreamAsync().ConfigureAwait(false);
var result = await processor(stream).ConfigureAwait(false);
await LogResultAsync(result).ConfigureAwait(false);
return result;
}
}
// Application code (ASP.NET Core): KHÔNG CẦN ConfigureAwait
public class OrderService
{
public async Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
var order = Order.Create(cmd);
await _repository.AddAsync(order); // No ConfigureAwait needed
await _eventBus.PublishAsync(new OrderCreated(order.Id));
return order;
}
}
SynchronizationContext
csharp// SynchronizationContext quyết định thread nào chạy continuation sau await
// WPF/WinForms: DispatcherSynchronizationContext → UI thread
// ASP.NET Framework: AspNetSynchronizationContext → request thread
// ASP.NET Core: NULL → ThreadPool thread (fast!)
// Console apps: NULL → ThreadPool thread
// ASP.NET Core KHÔNG CÓ SyncContext → await resume trên bất kỳ ThreadPool thread
// → Đây là lý do ASP.NET Core nhanh hơn ASP.NET Framework
Channel<T> - Producer/Consumer Pattern
csharp// Channel<T>: high-performance async queue
// Thay thế BlockingCollection cho async scenarios
public class BackgroundOrderProcessor : BackgroundService
{
private readonly Channel<OrderEvent> _channel;
public BackgroundOrderProcessor()
{
// Bounded channel: backpressure khi đầy
_channel = Channel.CreateBounded<OrderEvent>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.Wait, // Wait khi full
SingleReader = true, // 1 consumer → optimize
SingleWriter = false // nhiều producers
});
}
// Producer: non-blocking enqueue
public async ValueTask EnqueueAsync(OrderEvent evt, CancellationToken ct = default)
{
await _channel.Writer.WriteAsync(evt, ct);
}
// Consumer: background processing
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var evt in _channel.Reader.ReadAllAsync(stoppingToken))
{
try
{
await ProcessEventAsync(evt);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order event {OrderId}", evt.OrderId);
// Dead letter, retry, etc.
}
}
}
}
// Registration
builder.Services.AddSingleton<BackgroundOrderProcessor>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<BackgroundOrderProcessor>());
Parallel.ForEachAsync (.NET 6+)
csharp// Process items in parallel với concurrency control
public async Task SyncProductsAsync(IReadOnlyList<Product> products)
{
await Parallel.ForEachAsync(
products,
new ParallelOptions
{
MaxDegreeOfParallelism = 10, // Max 10 concurrent
CancellationToken = _ct
},
async (product, ct) =>
{
await _externalApi.SyncProductAsync(product, ct);
_logger.LogInformation("Synced product {ProductId}", product.Id);
});
}
// So sánh approaches:
// Task.WhenAll → tất cả chạy đồng thời, KHÔNG control concurrency
// Parallel.ForEachAsync → control concurrency, backpressure
// Channel<T> → producer/consumer, decouple
4. Memory Profiling & GC Tuning
Detecting Memory Issues
csharp// 1. dotnet-counters: live monitoring
// dotnet tool install -g dotnet-counters
// dotnet-counters monitor --process-id <PID> --counters System.Runtime
// Key metrics to watch:
// - gc-heap-size: tổng heap size
// - gen-X-gc-count: số lần GC mỗi generation
// - alloc-rate: allocation rate (bytes/sec)
// - exception-count: exception frequency
// - threadpool-queue-length: bao nhiêu work items đang chờ
// 2. dotnet-trace: capture trace for profiling
// dotnet tool install -g dotnet-trace
// dotnet-trace collect --process-id <PID> --duration 00:00:30
// → Mở file .nettrace trong Visual Studio / PerfView
// 3. dotnet-dump: analyze memory dump
// dotnet-dump collect --process-id <PID>
// dotnet-dump analyze <dump-file>
// > dumpheap -stat # Top objects by count/size
// > dumpheap -type Order # Tìm tất cả Order objects
// > gcroot <address> # Tại sao object chưa bị GC?
GC Configuration cho Production
xml<!-- runtimeconfig.json hoặc .csproj -->
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>
<!-- GC Heap Limit: giới hạn memory usage -->
<!-- Phù hợp container environments -->
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true,
"System.GC.Concurrent": true,
"System.GC.HeapHardLimit": 536870912, // 512MB hard limit
"System.GC.HeapHardLimitPercent": 75 // hoặc 75% container memory
}
}
}
Allocation-Free Coding Patterns
csharp// 1. stackalloc cho small buffers
public static bool IsValidHex(ReadOnlySpan<char> input)
{
Span<byte> bytes = stackalloc byte[input.Length / 2];
return Convert.TryFromHexString(input, bytes, out _);
}
// 2. String.Create thay StringBuilder cho simple cases
public static string FormatOrderId(int year, int sequence)
{
return string.Create(12, (year, sequence), (span, state) =>
{
"ORD-".AsSpan().CopyTo(span);
state.year.TryFormat(span[4..], out _);
span[8] = '-';
state.sequence.TryFormat(span[9..], out _, "D3");
});
// → "ORD-2024-001", zero intermediate allocations
}
// 3. SearchValues<T> (.NET 8+)
private static readonly SearchValues<char> _vowels =
SearchValues.Create("aeiouAEIOU");
public static int CountVowels(ReadOnlySpan<char> text)
{
int count = 0;
int index;
while ((index = text.IndexOfAny(_vowels)) >= 0)
{
count++;
text = text[(index + 1)..];
}
return count;
}
// → SIMD-accelerated search, cực nhanh
// 4. CompositeFormat (.NET 8+)
private static readonly CompositeFormat _logFormat =
CompositeFormat.Parse("Order {0} processed in {1}ms with status {2}");
public void LogOrderProcessed(Guid orderId, long elapsedMs, string status)
{
_logger.LogInformation(
string.Format(null, _logFormat, orderId, elapsedMs, status));
}
5. Diagnostic Scenarios Thực Tế
Scenario 1: Memory Leak Detection
csharp// Symptom: Gen 2 GC tăng liên tục, heap size không giảm
// Nguyên nhân phổ biến:
// 1. Event handler không unsubscribe
public class OrderObserver
{
public OrderObserver(IOrderService service)
{
service.OrderCreated += OnOrderCreated; // ❌ Never unsubscribed!
}
}
// Fix: implement IDisposable
public class OrderObserver : IDisposable
{
private readonly IOrderService _service;
public OrderObserver(IOrderService service)
{
_service = service;
_service.OrderCreated += OnOrderCreated;
}
public void Dispose() => _service.OrderCreated -= OnOrderCreated;
}
// 2. Static collections growing unbounded
private static readonly Dictionary<string, CacheEntry> _cache = new(); // ❌ Never cleaned!
// Fix: dùng MemoryCache với expiration
private readonly IMemoryCache _cache;
_cache.Set(key, value, TimeSpan.FromMinutes(30));
// 3. HttpClient mới mỗi request
// ❌ using var client = new HttpClient(); // Socket exhaustion!
// ✅ IHttpClientFactory (pooled connections)
Scenario 2: ThreadPool Starvation
csharp// Symptom: threadpool-queue-length tăng cao, response time tăng
// Nguyên nhân: blocking calls trên ThreadPool threads
// ❌ Sync over async
public ActionResult GetProducts()
{
var products = _db.Products.ToListAsync().Result; // 💥 Deadlock risk!
return Ok(products);
}
// ❌ Thread.Sleep trong async context
public async Task ProcessAsync()
{
await Task.Delay(100); // ✅ OK
Thread.Sleep(100); // ❌ Blocks ThreadPool thread
}
// ✅ Fix: properly async all the way
public async Task<ActionResult> GetProducts()
{
var products = await _db.Products.ToListAsync();
return Ok(products);
}
Tổng Kết Session 05
| Concept | Key Takeaway |
|---|---|
| BenchmarkDotNet | Luôn benchmark trước optimize, đo cả memory allocation |
| ObjectPool | Dùng cho objects tốn kém create/destroy |
| ArrayPool | Tránh GC pressure từ large array allocations |
| ValueTask | Dùng khi hot path có thể return synchronously |
| Channel<T> | Producer/consumer pattern cho background processing |
| Parallel.ForEachAsync | Parallel I/O-bound tasks với concurrency control |
| stackalloc + Span | Zero-allocation cho small buffer operations |
Bài tập: Profile 1 API endpoint trong project bằng
dotnet-counters. Xác định allocation hotspot, áp dụngArrayPoolhoặcSpan<T>, sau đó benchmark trước/sau bằng BenchmarkDotNet.