Parallel.ForEachAsync bize .NET 6 ile birlikte tanıtılmıştır. Bu metot .NET 6’nın getirdiği en önemli yeniliklerden birisi. Çünkü async metotların paralel döngü içinde verimli bir şekilde kullanılmasına olanak tanır.
Parallel.ForEach
Parallel.ForEach, .NET’in System.Threading.Tasks kütüphanesinde bulunan ve veri paralelliği sağlamak için kullanılan bir yapıdır. Temel amacı, bir koleksiyondaki her bir öğe üzerinde yapılacak bağımsız işleri birden fazla CPU çekirdeğine dağıtarak işleme süresini hızlandırmaktır.
Parallel.ForEach genellikle CPU-Bound işler için idealdir. Örneğin büyük bir listenin veya dizinin işlemden geçirilmesi, görüntü işlenmesi veya bir dosyanın şifrelenmesi gibi. Çünkü bunlar CPU meşgul eder.
Bu metot bize eş zamanlı olarak çalışan threads kullanır. Bakın “task” demedim, “concurrent” çalışan threads kullanır. Ancak döngü içindeki bir işlem dosya okuma, ağ isteği, veritabanı sorgusu gibi şeyler ise, ilgili thread bu operasyonun tamamlanmasını beklemek zorunda kalır. Bu bekleme sırasında thread hiçbir iş yapmaz, bu da sistem kaynaklarının verimsiz kullanılmasına neden olur.
var numbers = Enumerable.Range(1, 1000);
Parallel.ForEach(numbers, number =>
{
Console.WriteLine($"{number} (Thread ID: {Thread.CurrentThread.ManagedThreadId})");
});Parallel.ForEachAsync
.NET 6 ile gelen Parallel.ForEachAsync, geleneksel Parallel.ForEach’in kısıtlamasını aşmak için tasarlanmıştır. Bu metot üzerinde döngü içindeki işin async olmasını destekler. Özellikle I/O-Bound işler için mükemmel bir araçtır.
ForEachAsync kullanıldığında, bir ağ isteği veya dosya okuma gibi bir operasyon başlatıldığında, o görevi başlatan iş parçacığı bloke olmaz. Yani döngü içindeki thread bekleme süresi boyunca diğer görevleri işlemek üzere iş parçacığı havuzuna (ThreadPool) geri döner. İşlemi tamamlandığında sonuç bir callback ile işlenir. Bu sayede az sayıda thread ile çok sayıda eşzamanlı işlem verimli bir şekilde yönetilebilir.
Tıpkı Parallel.ForEach gibi, Parallel.ForEachAsync de bir ParallelOptions nesnesi alabilir. Bu nesne ile MaxDegreeOfParallelism ayarlanarak, aynı anda çalıştırılacak thread sayısı sınırlandırılabilir. Burayı “-1” yaparsanız sınırsız olduğunu gösterirsiniz. Lütfen buna dikkat edin.
Şimdi basit bir repository örneği üzerinden aynı anda birçok ürünü çağırabileceğimiz bir metot geliştirelim.
Bu örnekte veritabanından belirli ID’leri baz alarak veri getireceğiz. MaxDegreeOfParallelism = 4 olduğu için herhangi bir anda en fazla dört istek aynı anda bekleyebilir. İşlemler asenkron olduğu için, bu dört istek bekleme (sunucudan yanıt gelme) süresini boşuna bir thread tutarak geçirmez. Bu da uygulamanın genel yanıt verme hızını ve verimliliğini artırır.
using System.Threading.Tasks.Dataflow;
// Asynchronous interface for database operations
public interface IProductRepository
{
Task<Product> GetByIdAsync(int id, CancellationToken token = default);
}
public class MockProductRepository : IProductRepository
{
public async Task<Product> GetByIdAsync(int id, CancellationToken token = default)
{
var delay = id % 3 == 0 ? 500 : 200;
await Task.Delay(delay, token);
return new Product { Id = id, Name = $"Product {id}", Price = 100 + id };
}
}
public record Product(int Id, string Name, decimal Price);
public class ProductService
{
private readonly IProductRepository _repository = new MockProductRepository();
public async Task RunParallelQueryAsync()
{
var productIds = Enumerable.Range(101, 10).ToList();
Console.WriteLine($"Total {productIds.Count} products will be fetched in parallel.");
// Parallel options allow a maximum of 4 concurrent queries to the database.
ParallelOptions parallelOptions = new()
{
MaxDegreeOfParallelism = 4,
CancellationToken = CancellationToken.None
};
// Using Parallel.ForEachAsync to run I/O-Bound queries in parallel and asynchronously
await Parallel.ForEachAsync(productIds, parallelOptions, async (id, token) =>
{
var product = await _repository.GetProductByIdAsync(id, token);
Console.WriteLine($"Processed: {product.Name}, Price: {product.Price}");
});
}
}Örneğimizi inceleyelim.
Bir veritabanından veri çekmek, CPU’yu değil, diski ve ağı kullanmayı bekleyen bir işlemdir. Sorgunun tamamlanması için geçen sürenin büyük bir kısmı beklemedir.
await _repository.GetByIdAsync(id, token); satırına gelindiğinde, görev başlatılır ve görev tamamlanana kadar iş parçacığı havuza geri döner. Bu o iş parçacığının başka bir asenkron görevi (döngüdeki diğer işlem gibi) çalıştırmasına olanak tanır. Eğer Parallel.ForEach kullanılsaydı 4 iş parçacığı sadece bekleyerek kilitlenecekti. ForEachAsync ise binlerce eşzamanlı işlemi az sayıda iş parçacığı kullanarak yönetebilir.

Çalışma mekanizması şöyle özetlenir:
Görev başlatılır: ForEachAsync, 4 ayrı görevi (Task) eşzamanlı olarak başlatır.
Awaiting: Kod await _repository satırına ulaştığında, ürün verisi henüz hazır değildir. Bu bir I/O (veritabanı sorgusu) işlemidir ve sunucudan yanıt gelmesi beklenmektedir.
Thread release: await keyword kullanıldığı için o anda işi yürütülürken, kullandığınız işletim sistemi iş parçacığını (thread) anında ThreadPool’a geri gönderir. Yani thread bloke olmaz, serbest kalır ve diğer işleri yapabilir. En önemli özelliği zaten bu.
Concurrent: Veritabanı sorgularının tamamı arka planda eşzamanlı olarak çalışır. 5 sorgu beklerken, sistemin yalnızca bir veya iki aktif thread’i meşgul olurken işleminin tamamlanması beklenir. Bu ise yüzlerce eşzamanlı I/O işlemi yaparken bile CPU kaynaklarınızın (thread öğelerinin) verimli kullanılmasını sağlar.
Gerçekten çok kullanışlı ve sade bir şekilde concurrent task çalıştırmamıza olanak sağlıyor.
await Parallel.ForEachAsync(items, parallelOptions, async (id, token) =>
{
var pr = await _repository.GetProductByIdAsync(id, token);
Console.WriteLine($"Name: {pr.Name}");
});Parallel.ForEachAsync‘in temel katkısı, paralel çalıştırılan I/O işlemlerinin, iş parçacıklarını bloke etmesini engellemektir. Çalışan thread bekleme süresini diğer işler için kullanabildiği için performans ve verimlilik artar.
.NET’in her yeni sürümüyle işlerimizi kolaylaştıracak ve uygulamalarımıza değer katacak şeyleri görmek heyecan verici.



Yorum bırakın