Senkron & Asenkron ve MultiThread Programlama -9 — TaskScheduler & Context Switching
TaskScheduler & Context Switching
TaskScheduler
- TaskScheduler, .NET ortamında görevlerin (tasks) nasıl yürütüleceğini kontrol eden bir bileşendir. Task sınıfı ile oluşturulan görevlerin hangi sırada ve hangi thread üzerinde çalışacağını belirler.
- TaskScheduler, özellikle asenkron programlama ve çok iş parçacıklı uygulamalarda görevlerin yönetimini kolaylaştırır.
- TaskScheduler sınıfı, özellikle birden fazla Thread arasında veya işlemcilerde iş yükünü dağıtmak ve optimize etmek için kullanılır.
- Tüm bunların dışında TaskScheduler sınıfı özelleştirilerek farklı bir davranışta sağlanabilmektedir.
Özetle .NET TaskScheduler, görevlerin nasıl ve hangi sırayla yürütüleceğini belirler. Varsayılan olarak ThreadPool kullanılsa da, uygulamanızın ihtiyaçlarına göre özel bir TaskScheduler oluşturabilirsiniz.
UI senaryolarında veya kaynak kısıtlaması olan ortamlarda özel planlayıcılar, performansı ve verimliliği artırabilir.
TaskScheduler ve Task.Factory.StartNew Kullanımı
- TaskFactory, görevlerin oluşturulup planlanmasını kolaylaştırır ve özelleştirilmiş bir TaskScheduler’ı görevlerle ilişkilendirmenin ana yoludur.
Task.Factory.StartNew(() =>
{
//...
},
CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
- Bu örnekte TaskScheduler.Default özelliği kullanılarak ilgili Task, varsayılan planlayıcıya gönderilmektedir.
TaskFactory TaskScheduler Parametreleri
CancellationToken
Amaç: Task’ların iptal edilebilmesini sağlar. Bir CancellationToken
, dışarıdan gönderilen bir iptal isteğiyle task'ı sonlandırabilir.
CancellationToken.None
:
- Eğer task için iptal mekanizması kullanmak istemiyorsanız bu sabiti kullanabilirsiniz. Bu durumda task iptal edilmeye karşı duyarsız olur.
- Eğer bir cancellationToken kullanmak isterseniz, aşağıdaki örnekdeki gibi bir tane olusturup TaskFactory’e birinci parametre olarak gönderebilirsiniz.
var cts = new CancellationTokenSource();
Task.Factory.StartNew(() =>
{
for (int i = 0; i < 10; i++)
{
if (cts.Token.IsCancellationRequested)
{
Console.WriteLine("Task iptal edildi.");
return; // İptal durumunda task sonlanır.
}
Console.WriteLine($"İşlem {i}");
Thread.Sleep(1000);
}
}, cts.Token);
// 3 saniye bekleyip task'ı iptal edelim
Thread.Sleep(3000);
cts.Cancel();
TaskCreationOptions
Amaç: Task’ın nasıl oluşturulacağını ve nasıl çalışacağını kontrol eder. Aşağıda yaygın kullanılan seçenekler ve anlamları açıklanmıştır:
TaskCreationOptions.None
:- Varsayılan olarak task’ın normal bir şekilde çalışmasını sağlar. Ekstra bir davranış eklenmez.
TaskCreationOptions.LongRunning
:- İşlem çok uzun sürecekse bu seçenek kullanılır. Yeni bir thread oluşturulur ve mevcut ThreadPool’dan bağımsız olarak çalışır.
TaskCreationOptions.PreferFairness
:- Task’ların adil bir şekilde sıralanmasını önerir, yani önce oluşturulan task’ların önce çalışması beklenir. Ancak garanti vermez.
TaskCreationOptions.AttachedToParent
:- Bu seçenek, task’ı bir ana task’a bağlar. Eğer parent task tamamlanmadan child task tamamlanmazsa, ana task’ın sonlanması gecikebilir.
var parentTask = Task.Factory.StartNew(() =>
{
Console.WriteLine("Ana task başladı.");
Task.Factory.StartNew(() =>
{
Thread.Sleep(2000);
Console.WriteLine("Child task tamamlandı.");
}, TaskCreationOptions.AttachedToParent);
});
parentTask.Wait(); // Child task tamamlanmadan parent sonlanmaz
Console.WriteLine("Ana task bitti.");
Custom TaskScheduler
- Yazının başında da söylediğimiz gibi, TaskScheduler.Default diyerek TaskScheduler’in default davranış sergilemesi talebinin yanında, Custom bir taskScheduler sınıfı olusturarak da, Task’ların işlenmesi üzerinde, o anki ihtiyaca yönelik farklı özel davranışlar kazandırabiliriz.
public class LimitedConcurrencyScheduler : TaskScheduler
{
private readonly LinkedList<Task> _tasks = new LinkedList<Task>();
private readonly SemaphoreSlim _semaphore;
public LimitedConcurrencyScheduler(int maxConcurrency)
{
_semaphore = new SemaphoreSlim(maxConcurrency);
}
protected override IEnumerable<Task>? GetScheduledTasks()
{
lock (_tasks)
{
return _tasks.ToArray(); // Bekleyen task'ları döndür
}
}
protected override void QueueTask(Task task)
{
lock (_tasks)
{
_tasks.AddLast(task); // Yeni task'ı kuyruğa ekle
}
ThreadPool.QueueUserWorkItem(async _ =>
{
await _semaphore.WaitAsync();
try
{
TryExecuteTask(task); // Task'ı çalıştır
}
finally
{
lock (_tasks)
{
_tasks.Remove(task); // Task'ı kuyruktan çıkar
}
_semaphore.Release(); // Semaphore serbest bırak
}
});
}
- Bu örnekte, LimitedConcurrencyScheduler adında özel bir TaskScheduler sınıfı oluşturulmuştur ve bu sınıfın QueueTask metodunda ThreadPool.QueueUserWorkItem komutu eşliğinde ilgili Task farklı bir Thread’de çalıştırılmıştır.
Oluşturulan custom task scheduler aşağıdaki gibi kullanılabilmektedir.
var scheduler = new LimitedConcurrencyScheduler(2);
var tasks = new List<Task>();
for (int i = 1; i <= 5; i++)
{
int taskNum = i;
tasks.Add(Task.Factory.StartNew(() =>
{
Console.WriteLine($"Task {taskNum} başladı.");
Thread.Sleep(2000); // İşlem simülasyonu
Console.WriteLine($"Task {taskNum} tamamlandı.");
}, CancellationToken.None, TaskCreationOptions.None, scheduler));
}
Task.WaitAll(tasks.ToArray());
Bu örnekte olduğu gibi, farklı senaryolardaki ihtiyaç duyulan çözümlere yönelik davranış sergileyecek TaskScheduler’ları tasarlayarak projelerimizde kullanabiliriz.
Maalesef
Task.Run
ile yapılan çağrılarda özel birTaskScheduler
(CustomTaskScheduler) kullanamazsınız.Task.Run
, varsayılan olarakTaskScheduler.Default
'u kullanır ve bu davranışı değiştirmek için bir seçenek sunmaz.Eğer özel bir
TaskScheduler
kullanmak istiyorsanız,Task.Factory.StartNew
veyaTaskFactory
sınıfını kullanmanız gerekir. Bu sınıflar, özel birTaskScheduler
belirtmenize izin verir ve bu sayede kendi tanımladığınızTaskScheduler
'ı kullanabilirsiniz.
Doğru yapılandırılmış bir task scheduler, uygulamanın performansını artırmanın yanı sıra kaynakları verimli kullanarak iş yüklerini dengeli bir şekilde yönetir. Bu sayede, sistem hem stabil hem de ölçeklenebilir hale gelirken, karmaşık iş akışları daha sorunsuz ve etkili bir şekilde yürütülür.
Context Switching Nedir?
Context switching, bir işletim sisteminde CPU’nun, bir işlemden (veya thread’den) diğerine geçerken her birinin durumunu kaydedip yükleme sürecine verilen isimdir. Bu süreç, sistemin aynı anda birden fazla işlemi veya thread’i yönetmesini sağlar. CPU, bir işlemi bırakıp diğerine geçerken eski işlemin tüm çalışma bilgilerini (CPU kayıtları, program sayacı, çağrı yığını vb.) kaydeder ve yeni işlemin bilgilerini yükler. Böylece, her işlem bıraktığı yerden devam edebilir.
.NET geliştirme süreçlerinde çok iş parçacıklı (multi-threaded) yapılar sıkça kullanılır. Bu durumda, context switching’in uygulamanın performansına olan etkisini anlamak oldukça önemlidir.
Threads ve Task Parallel Library (TPL)
.NET’in Task Parallel Library (TPL) ve async/await desenleri, eşzamanlı işlemler için güçlü araçlar sunar. Ancak, bir task await
nedeniyle beklemeye alındığında veya thread bloklandığında, bir context switch gerçekleşir. Bu geçiş, kaçınılmaz olsa da maliyetlidir. CPU’nun her geçişte bağlamı kaydetmesi ve geri yüklemesi zaman alır ve çok sayıda thread veya task kullanımı bu yükü artırabilir.
Sürekli context switching, uygulamanın gerçek işleme süresini azaltarak performans darboğazlarına yol açabilir. Bu yüzden context switching’in etkisini en aza indirmek, performansı artırmak açısından kritik bir konudur.
.NET’te Context Switching’i Optimize Etme
Context switching tamamen ortadan kaldırılamaz, ancak minimize edilebilir. İşte bazı ipuçları:
- Async-Await’i Verimli Kullanmak: Async/await yapısını yalnızca I/O tabanlı işlemler için kullanmak daha doğrudur. CPU yoğun işlemlerde, paralel işleme yöntemleri veya daha kücük işlemlerde senkron çalışma tercih edilmelidir. Gereksiz await kullanımı, fazladan context switch oluşturabilir.
- ThreadPool Kullanımını Optimize Etmek: .NET uygulamaları, task’leri yürütmek için bir thread havuzu (ThreadPool) kullanır. Ancak aşırı yüklenen bir ThreadPool, performansı olumsuz etkileyebilir. Bu nedenle, thread yönetimini izlemek ve optimize etmek önemlidir.
- Senkronizasyon Yapılarını Doğru Kullanmak: Locks, SemaphoreSlim, Mutex gibi senkronizasyon yapıları, bloklanma yaratarak context switch’e sebep olabilir. Gereksiz bloklamaları önlemek için bu yapıların doğru kullanılması gerekir.
Thrashing: Context Switching Yanlış Gittiğinde
Thrashing, sistemin gerçek iş yapmaktan çok, sürekli context switch ile uğraşması anlamına gelir. Bu durum, bir cambazın fazla sayıda topu tutamayıp düşürmesine benzetilebilir. Uygulama, aşırı thread kullanımı veya kötü task yönetimi yüzünden sürekli kaynak değiştirir, ancak işleme konusunda ilerleme sağlayamaz.
Thrashing Nedenleri
- Verimsiz Task Yönetimi: Gereksiz veya yanlış yapılandırılmış task’lerin fazla tetiklenmesi, yani Async-await’in gereksiz ve hatalı kullanımı ThreadPool’un aşırı yüklenmesine ve dolayısıyla sürekli context switch’e ve thrashing’e yol açabilir.
- Fazla Thread Oluşturma: Dolaylı yoldan birden fazla thread oluşturulması, sistemin kaldırabileceğinden fazla yük yaratabilir.
- Aşırı Lock Kullanımı: Çok fazla thread’in aynı lock’a erişmeye çalışması performansı düşürür.
Thrashing Belirtileri
- Uygulamanın yavaşlaması ve yanıt vermemesi.
- Yüksek CPU kullanımı, ancak işlem tamamlama oranının düşük olması.
- Paralel çalışan işlemlerin beklenmedik şekilde bloklanması.
Thrashing’i Önlemek İçin Stratejiler
- Uygun sayıda thread kullanarak ThreadPool’u verimli yönetin. Benchmark testleri ile ideal thread sayısını belirleyin.
- Async/await yapısını doğru yerlerde kullanarak gereksiz bloklamaların önüne geçin.
- Performans izleyici araçlar ile uygulamanızı izleyip darboğazları tespit edin ve optimize edin.
Gerçek Dünya Senaryoları
Senaryo 1: Web Uygulaması Performansı
Yoğun trafikli bir web uygulamasında, async/await hatalı kullanıldığında thread havuzu gereksiz yere doldu ve thrashing oluştu. Bu, yanıt sürelerini artırarak performans düşüşüne neden oldu. Async/await işlemleri optimize edilip thread kullanımı dengelendiğinde, performans gözle görülür şekilde iyileştirildi.
Senaryo 2: Veri İşleme Uygulaması
Karmaşık veri işleme uygulamalarında küçük taskler için aşırı sayıda thread oluşturulması, fazla context switching’e yol açtı. Uygulamanın task yapısı yeniden düzenlenip paralel döngüler optimize edildiğinde yürütme süresi önemli ölçüde kısaldı.
Örnek: Verimsiz Async-Await Kullanımı
Bir .NET uygulaması, bir döngüde birden fazla web hizmetinden veri alıyor. Verimsiz versiyonda, her çağrıyı senkron olarak bekliyor, bu da gereksiz bloklamaya yol açıyor.
Verimsiz örnek:
public class DataService
{
public async Task<List<string>> FetchDataFromServicesAsync(List<Uri> serviceUrls)
{
List<string> results = new List<string>(serviceUrls.Count);
foreach (var url in serviceUrls)
{
// Verimsiz: Her bir çağrı için senkron olarak bekleme
var result = await FetchDataFromService(url);
results.Add(result);
}
return results;
}
private async Task<string> FetchDataFromService(Uri url)
{
using (var httpClient = new HttpClient())
{
// Asenkron I/O operasyonunu simüle etme
return await httpClient.GetStringAsync(url).ConfigureAwait(false);
}
}
}
İyileştirilmiş versiyon:
public class DataService
{
public async Task<string[]> FetchDataFromServicesAsync(List<Uri> serviceUrls)
{
var tasks = serviceUrls.Select(FetchDataFromService);
// Verimli: Tüm Tasklerin tamamlanmasını asenkron olarak bekleme
var results = await Task.WhenAll(tasks);
return results;
}
private async Task<string> FetchDataFromService(Uri url)
{
using (var httpClient = new HttpClient())
{
// Asenkron I/O operasyonunu simüle etme
return await httpClient.GetStringAsync(url).ConfigureAwait(false);
}
}
}
Task.WhenAll kullanarak, metod şimdi tüm web hizmeti çağrılarını eşzamanlı olarak bekler, bu da genel bekleme süresini azaltır. Bu context switch’leri ortadan kaldırmaz, ancak asenkron işlemleri gruplandırarak, thread bloklamasını azaltarak ve ThreadPool’u daha iyi kullanarak bunları daha verimli yönetir.
Sonuç
Context switching, çok iş parçacıklı ve asenkron uygulamalarda kaçınılmaz bir süreçtir, ancak doğru stratejilerle etkisi en aza indirilebilir. .NET geliştiricileri olarak, async-await kullanımını optimize etmek, thread havuzunu yönetmek ve senkronizasyon yapılarını dikkatli kullanmak, uygulamalarınızda daha yüksek performans elde etmenize yardımcı olacaktır.
Kaynaklar
TaskScheduler: Gençay Yıldız — Asenkron & Multithread Programlama
Context Switching: https://codingbolt.net/2024/01/20/understanding-context-switching-in-the-net-world/