Senkron & Asenkron ve MultiThread Programlama -8 — TPL & Task sınıfı & Async Await & ConfigureAwait

Alperen Öz
12 min read2 hours ago

--

TPL & Task sınıfı

Photo by Bethany Legg on Unsplash

TPL, yani Task Parallel Library, .NET Framework ve .NET Core’da yerleşik olarak bulunan bir kütüphanedir. Asenkron ve paralel programlamayı daha kolay ve verimli bir şekilde gerçekleştirmek için kullanılır. TPL, geliştiricilere çok iş parçacıklı (multithreaded) ve paralel işlem gerektiren uygulamaları yazmada yardımcı olur, böylece uygulamaların performansını artırır ve işlem sürelerini kısaltır.

Temel Özellikler:

  1. Task Tabanlı Programlama: TPL, işlemleri task’lar (görevler) ile yürütmenizi sağlar. Task’lar, basitçe bir iş parçası üzerinde gerçekleştirilen bir işlemi temsil eder. Task ve Task<TResult> sınıfları, TPL'nin temel yapı taşlarıdır.
  2. Paralel Döngüler: Parallel.For ve Parallel.ForEach gibi metotlarla, döngüleri paralel olarak çalıştırabilirsiniz. Bu sayede aynı işlemin farklı veri parçaları üzerinde eşzamanlı yürütülmesi sağlanır.
  3. Asenkron Programlama: TPL, async/await ile birlikte kullanıldığında, uzun süren işlemlerin asenkron olarak yürütülmesine olanak tanır. Bu, kullanıcı arayüzünün donmamasını ve uygulamanın daha hızlı tepki vermesini sağlar.
  4. Thread Pool Kullanımı: TPL, .NET Framework’te ThreadPool’u daha verimli kullanarak iş parçacıklarını yönetir. TPL, Task Scheduler ile iş yüklerini otomatik optimize eder. Task’lar, manuel thread kullanımına göre daha basit ve etkili bir çözüm sunar.
  5. Hata Yönetimi: TPL ile yapılan asenkron işlemlerde hata yönetimi daha kolaydır. Task’lar üzerinden hata durumları daha organize bir şekilde yakalanabilir. try-catch blokları ile hata yönetimi daha temiz bir şekilde yapılır.

Task Sınıfı

  • Task sınıfı, C# dilinde asenkron işlemleri temsil etmek için kullanılan, oldukça yaygın bir sınıftır.
  • Task sınıfı, belirli işlemleri yapmak için aslında yeni bir thread’i temsil eder ve böylece main thread’i bloklamadan diğer işlemlere devam edebilir.

Task sınıfı her zaman yeni bir thread oluşturmaz. Çoğunlukla ThreadPool'dan thread kullanır. Bu yüzden Task nesnesinin her zaman yeni bir thread ile çalıştırılacağı garantisi yoktur.

  • Task ile Thread sınıfları aslında aynı amaca hizmet etse de, farklı thread yönetim yaklaşımlarını temsil etmektedirler ve hangisinin tercih edilmesi gerektiği, kullanım senaryolarına ve gereksinimlere göre değişiklik göstermektedir.
  • Thread daha düşük seviyeli, donanıma yakın bir yapı sunarken Task, TPL sayesinde paralel ve asenkron iş akışlarını daha verimli bir şekilde yönetir.

Task ve Thread Arasındaki Farklar

Task

  • Asenkron programlamada kullanılır.
  • Task, async ve await keyword’leriyle birlikte modern C# dilinde daha kolay kullanım sağlar.
  • .NET tarafından sağlanan otomatik bir yönetime sahiptir. Thread pool kullanarak thread’leri daha etkin bir şekilde yönetebilir.
  • Asenkron süreçlerde daha fazla kontrol sağlar.
  • Otomatik bir yapı söz konusu olduğu için kaynakları daha etkin bir şekilde kullanır.

Thread

  • Daha düşük seviyede thread yönetimi gerektiren özel senaryolarda kullanılır.
  • Doğrudan thread oluşturulmasını ve kontrol edilmesini sağlar.
  • Daha fazla sistem kaynağı tüketmektedir ve işlemci sistemini tarifi yöneten thread mekanizması sağlamaktadır.
  • Daha düşük seviyede paralel işlemler veya iş parçacıkları için ideal olabilir.
  • Thread’ler doğrudan yönetildiği için kaynak yönetimi maliyetli olabilmektedir.

Thread sınıfı, genel bakışta Task sınıfına göre thread yönetimi konusunda daha manuel ve düşük seviyede olabilir. Fakat bu Thread sınıfına hiçbir zaman ihtiyaç yok demek değildir. Senaryoya göre thread’lerin manuel ve daha düşük seviyede kontrolü ihtiyaç karşılamada daha iyi olabilir.

Task sınıfı ve Thread sınıfı, farklı kullanım senaryoları ve ihtiyaçlar için tasarlanmıştır. Task sınıfı, asenkron ve kısa süreli işlemler için idealdir ve ThreadPool’dan faydalanarak kaynakları verimli kullanır. Thread sınıfı ise daha düşük seviyede kontrol ve uzun süreli işlemler için uygundur. Her iki yaklaşımın da kendine özgü avantajları vardır ve kullanacağınız senaryoya göre doğru aracı seçmek önemlidir.

Task Sınıfı ve ThreadPool

  • Task sınıfı, asenkron işlemleri temsil eder ve çalıştırır. Task sınıfı, işlemleri gerçekleştirmek için arka planda ThreadPool’dan iş parçacıklarını alır.
  • Task.Run, Task.Factory.StartNew gibi metodlar kullanıldığında, Task nesnesi oluşturulur ve bu nesne arka planda bir ThreadPool iş parçacığı üzerinde çalıştırılır.
  • Task sınıfı, .NET’in sağladığı TPL (Task Parallel Library) ile birlikte çalışarak, paralel programlama ve iş parçacığı yönetimini daha kolay ve esnek hale getirir.
  • Task’ların ThreadPool ile entegrasyonu, kaynakların daha verimli kullanılmasını sağlar ve sistem performansını artırır.
Task task = Task.Run(() =>
{
// İşlemler burada gerçekleştiriliyor...
// Bu işlemler, arka planda ThreadPool'dan bir iş parçacığı tarafından yürütülür.
});

Task Sınıfı Nasıl Kullanılır?

Bir asenkron süreç başlatabilmek için Task instance’ına ihtiyacımız olacaktır. Haliyle bunun için üç farklı yöntemle Task instance’ı oluşturabiliriz.

  1. new Task()

Bu yöntem klasik Task sınıfından bir instance üretme yöntemidir. Bu üretilecek Task henüz başlatılmamış bir Task olacaktır. Yanı bir işlemi temsil edecek, ancak henüz çalıştırılmadığı anlamına gelecektir. Haliyle bu durumda Task’ı çalıştırmak için Start metodunun çağrılması gerekmektedir.

Task task = new Task(() =>
{
// İşlemler burada gerçekleştiriliyor...
});
task.Start();

2. Task.Run()

Bu metot ile ThreadPool üzerinden alınmış ve başlatılmış bir Task nesnesi elde edilir. Bu metot, en yaygın ve basit kullanıma sahiptir.

Task task = Task.Run(() =>
{
// İşlemler burada gerçekleştiriliyor...
});

3. Task.Factory.StartNew()

Task.Run ile task oluşturmak ve çalıştırmak, daha kısa ve kolay bir kullanım sunar. Task.Factory.StartNew ise daha fazla özelleştirme imkanı sağlar ve kullanım esnekliği sunar. Ancak iki yöntem arasında bazı farklılıklar vardır.

Hem Task.Run hem de Task.Factory.StartNew ile başlatılan bir task içinde alt tasklar çalışabilir. Alt taskların üst task’a bağlı olarak çalışmasını sağlamak için TaskCreationOptions.AttachedToParent seçeneği kullanılmalıdır. Aksi halde alt tasklar bağımsız şekilde çalışır.

Task.Factory.StartNew ile oluşturulan görevler, varsayılan olarak ThreadPool’dan thread kullanır. Ancak, TaskCreationOptions.LongRunning seçeneği ile görev için yeni bir thread başlatılabilir. Bu işlemler TaskScheduler tarafından optimize edilir ve yönetilir.

Genel olarak Task.Run, basit senaryolar için tercih edilirken, daha karmaşık konfigürasyonlar gerektiğinde Task.Factory.StartNew kullanılır.

Task task = Task.Factory.StartNew(() =>
{
// İşlemler burada gerçekleştiriliyor...
}, new(), TaskCreationOptions.None, TaskScheduler.Default);

Örnekte TaskFactory üzerinden Task baslatmanın sağladığı esneklikleri, parametreleri ile birlikte görebiliyoruz.

Task Sınıfı Metotları

Start

Bir Task instance’ını başlatır ve çalıştırır.

Task task = new Task(() =>
{
// Transactions here...
});
task.Start();

Wait

Bir Task’ın işleminin tamamlanmasını bekler.

Task task = new Task(() =>
{
// Transactions here...
});
task.Start();
task.Wait();

ContinueWith

Bir Task tamamlandıktan sonra belirli bir işlemi gerçekleştirmek için kullanılır.

Task task = Task.Run(() =>
{
// Transactions here...
});

task.ContinueWith(previousTask =>
{
Console.WriteLine("Task is completed.");
});

WaitAll

Verilen tüm Task’ların tamamlanmasını bekler.

Task.WaitAll(
Task.Run(() => Console.WriteLine("Task 1")),
Task.Run(() => Console.WriteLine("Task 2")),
Task.Run(() => Console.WriteLine("Task 3"))
);

WaitAny

Belirtilen herhangi bir Task’ın tamamlanmasını bekler.

Task.WaitAny(
Task.Run(() => Console.WriteLine("Task 1")),
Task.Run(() => Console.WriteLine("Task 2")),
Task.Run(() => Console.WriteLine("Task 3"))
);

WhenAny

Belirtilen herhangi bir Task tamamlandığında bir Task döndürür.

Task.WhenAny(
Task.Run(() => Console.WriteLine("Task 1")),
Task.Run(() => Console.WriteLine("Task 2")),
Task.Run(() => Console.WriteLine("Task 3"))
).ContinueWith(completedTask =>
{
Console.WriteLine("One of the tasks completed.");
});

WhenAll

Belirtilen tüm Task’lar tamamlandığında bir Task döndürür.

Task.WhenAll(
Task.Run(() => Console.WriteLine("Task 1")),
Task.Run(() => Console.WriteLine("Task 2")),
Task.Run(() => Console.WriteLine("Task 3"))
).ContinueWith(allTasks =>
{
Console.WriteLine("All tasks completed.");
});

Buradaki WhenAny ve WhenAll gibi geriye Task döndüren metotlarda ilgili bekleme işlemi, diğer geriye task döndürmeyen bekleme metotlarındaki gibi main thread’de degil, ayrı bir Task nesnesi üzerinde yapılmaktadır. Yani bekleme davranışı asenkron yürütülmektedir. Buradaki bekleme işleminde senkronluğu sağlayabilmek için await keywordüyle bir davranış sergilenmelidir.

Delay

Belirli bir süre boyunca thread’i bekleten metottur.

Task.Run(async () =>
{
await Task.Delay(15000);
// İşlemler burada gerçekleştirilir...
});

Burada delay metodunda da bir önceki bahsedilen konuya benzer bir durum vardır. Await keywordü olmadan buradaki delay talebi 15000 ms olmasına rağmen delay işlemi farklı bir Task üzerinde asenkron olarak yapılacağı için await keywordü olmadan ilgili task 15000 ms beklemeden işlemine devam edecekti.

FromCancelled

İptal edilmiş bir Task döndürür.

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
Task task = Task.Run(() =>
{
while (!cancellationTokenSource.IsCancellationRequested)
{
// Görevi iptal edene kadar işlemler burada gerçekleştirilir...
}
}, cancellationTokenSource.Token);

cancellationTokenSource.Cancel();
Task cancelledTask = Task.FromCanceled(cancellationTokenSource.Token);
if (cancelledTask.IsCanceled)
{
Console.WriteLine("Task cancelled.");
}

FromException

Hata ile sonuçlanmış bir Task’ı temsil eden Task nesnesi döndürür.

Task faultedTask = Task.FromException(new Exception("An error occurred."));

FromResult

Belirtilen bir değeri temsil eden Task nesnesi döndürür.

Task<int> resultTask = Task.FromResult(35);

Task.Result

  • Bu ifade, senkron olarak asenkron işlemin tamamlanmasını bekler ve sonucu döndürür.
  • Ancak UI thread’inde veya ASP.NET context’inde kullanıldığında deadlock (kilitlenme) riski vardır. Çünkü Result, çalıştığı thread'i bloklayarak işlemin tamamlanmasını bekler.

Task.GetAwaiter().GetResult()

  • Bu kullanım, aynı sonucu verir ama deadlock riskini azaltır.
  • Çünkü GetAwaiter().GetResult() ile, task’in async/await mekanizması bloklanmadan senkron sonuç elde edilebilir.
Task<int> task = Task.Run(() => 42);
var result = task.Result;
var result2 = task.GetAwaiter().GetResult();

Yield

Bir Task’ı geçici olarak bekletmemizi ve işlemci kaynağını diğer thread’lere vermemizi sağlayan metottur.

await Task.Yield();
// Transactions here...

Task Sınıfı Property’leri

CompletedTask

CompletedTask property’si, tamamlanmış bir Task nesnesi döndürür. Bu, özellikle bazı senaryolarda hemen tamamlanmış bir Task’a ihtiyaç duyduğunuzda kullanışlıdır.

Task completedTask = Task.CompletedTask;
if (completedTask.IsCompleted)
{
Console.WriteLine("The task is already completed.");
}

CurrentId

CurrentId property’si, o anda yürütülen Task’ın Id değerini döner. Bu, özellikle Task’ların hangi sırayla çalıştığını veya hangi Task’ın çalışmakta olduğunu anlamak için kullanışlıdır.

Task task = Task.Run(() =>
{
int? taskId = Task.CurrentId;
Console.WriteLine($"Current Task Id: {taskId}");
});
task.Wait();

Factory

Yeni Task nesneleri oluşturmak için kullanılan property’dir.

Task task = Task.Factory.StartNew(() =>
{
Console.WriteLine("Task is running.");
});

IsCompleted

Task’ın tamamlanıp tamamlanmadığını değerlendirir.

Task task = Task.Run(() => { });
task.Wait();
bool isCompleted = task.IsCompleted; // True

IsCanceled

Task’ın iptal edilip edilmediğini değerlendirir.

CancellationTokenSource cts = new CancellationTokenSource();
Task task = Task.Run(() =>
{
while (!cts.Token.IsCancellationRequested)
{
// transactions
}
}, cts.Token);

cts.Cancel();
bool isCanceled = task.IsCanceled; // True

IsCompletedSuccessfully

Task’ın başarılı bir şekilde tamamlanıp tamamlanmadığını değerlendirir.

Task task = Task.Run(() => { });
task.Wait();
bool isCompletedSuccessfully = task.IsCompletedSuccessfully; // True

Id

Task’ın Id’sini döndürür.

Task task = Task.Run(() => { });
int taskId = task.Id;

IsFaulted

Task’ın bir hata ile sonuçlanıp sonuçlanmadığını belirtir.

Task task = Task.Run(() => throw new Exception("Error"));
try
{
task.Wait();
}
catch { }

bool isFaulted = task.IsFaulted; // True

Status

Task’ın durumunu belirtir.

Task task = Task.Run(() => { });
task.Wait();
TaskStatus status = task.Status; // RanToCompletion

AsyncState

Task ile ilişkilendirilmiş state verisini elde etmenizi sağlar.

Task task = Task.Factory.StartNew((state) =>
{
Console.WriteLine(state);
}, "test");

var state = task.AsyncState; // "test"

Task Sınıfı ile İlgili Kritik Bilgiler

  • Task sınıfı altında çalışan görevler genellikle .NET’in görev paralelliği konsepti üzerine kurulmuştur. Ancak bu, her zaman farklı thread oluşturulduğu anlamına gelmemektedir.
  • Task sınıfı, .NET Task Parallel Library (TPL) tarafından sağlanan yüksek seviyeli bir mekanizmadır ve bu mekanizmada thread yönetimi otomatik olarak ele alınmaktadır.
  • Task.Factory.StartNew ile oluşturulan görevler, Thread Pool’dan bir thread alabilir veya yeni bir thread oluşturabilirler. Buradaki yönetim Task Scheduler tarafından optimize edilir.
  • Task Scheduler, bir görev başlatıldığında bunu Thread Pool’daki mevcut thread’lerden birinde çalıştırabileceği gibi yeni bir thread de oluşturabilir. Hangi Task’ın hangi thread’de çalışacağına dair bir garanti söz konusu değildir.
  • Dolayısıyla Task sınıfı için Thread Pool’un daha etkin bir şekilde kullanılmasına olanak tanıyan modern ve esnek bir yaklaşımdır diyebiliriz. Ancak belirli durumlarda, ihtiyaçlara göre doğrudan Thread sınıfını kullanmak daha uygun olabilir.
  • Özellikle belirli bir thread’i yönetme ihtiyacı varsa veya daha düşük seviyeli bir kontrol isteniyorsa Thread kullanılabilir. Ancak bu yaklaşım günümüzde asenkron programlama süreçlerinde TPL ve Task sınıfına karşı daha az tercih edilmektedir.

Async & Await

Async

  • C# dilinde async keyword'ü, bir metodun, lambda ifadesinin veya anonim metodun asenkron bir işlem gerçekleştireceğini belirtmek için kullanılır. Bu keyword, asenkron programlamayı daha basit, daha okunabilir ve daha yönetilebilir hale getirir.

async Keyword'ünün Temel Kullanımı

  • async keyword'ü, bir metodun asenkron olarak çalışacağını belirtir.
  • async olarak işaretlenen metotlar, genellikle bir Task veya Task<T> döndürür.
  • async keyword'ü, metodun içinde await ifadesinin kullanılabileceğini belirtir. await, asenkron bir işlemin tamamlanmasını beklemek için kullanılır.

async Keyword'ünün İmzalanması

Bir metodu async olarak işaretlemek için, metot imzasında async keyword'ünü eklemek gerekir. İşte basit bir örnek:

public async Task MyAsyncMethod()
{
// Asenkron işlemler burada gerçekleştirilir
await Task.Delay(1000); // 1 saniye bekleme
}

Bu örnekte, MyAsyncMethod metodu async olarak işaretlenmiştir ve içerisinde await ifadesi kullanılmıştır.

async Keyword'ü ile Task ve Task<T>

  • async metotlar genellikle bir Task veya Task<T> döndürür. Bu, metodun asenkron bir operasyonu tamamlayıp tamamlamadığını kontrol etmek için kullanılır.
  • Task döndüren metotlar geriye bir değer döndürmezken, Task<T> döndüren metotlar geriye bir değer döndürür.

Örnek: Task Döndüren Async Metot

public async Task PerformOperationAsync()
{
await Task.Delay(1000); // 1 saniye bekleme
// Diğer asenkron işlemler
}

Örnek: Task<T> Döndüren Async Metot

public async Task<int> CalculateValueAsync()
{
await Task.Delay(1000); // 1 saniye bekleme
return 42; // İşlem sonucu
}

async Keyword'ü ile Void Kullanımı

  • async metotlar nadiren void dönüş tipi ile kullanılır. void döndüren async metotlar, genellikle olay işleyicileri (event handlers) için kullanılır.

*void döndüren async metotlar, hata yönetimi ve sonuç kontrolü açısından zorluklar yaratabilir.

Neden Async Void Dönüş Tipinden Kaçınmalıyız?

Kod Akışında Belirsizlik

  • Async void metotlar, senkron kod ile asenkron kod arasındaki sınırları bulanıklaştırır. Haliyle kodun akışını ve kontrolünü zorlaştırır.

Çağıran Tarafından Kontrol Edilemezlik

  • Async void metotlarda meydana gelen hatalar, çağıran koda geri bildirilmezler! Bu durum, hataların fark edilmesini ve yönetilmesini zorlaştırır.

Geri Dönüş Değeri Olmaması

  • Async void metotlar geri dönüş değeri olmadığından, çağıran taraf bu metotların tamamlanıp tamamlanmadığını kontrol edemez. Bu, özellikle bir işlemin tamamlanmasını beklemek gerektiğinde sorun yaratır.

Hata Yönetimi Zorluğu

  • Örneğin, async Task döndüren bir metodun hata yönetimi try-catch ile yapılırken, async void metodun hataları yakalanamaz!

Await

  • await keyword'ü, asenkron bir işlemin tamamlanmasını beklemek için kullanılır.
  • await ifadesi, yalnızca async ile işaretlenmiş metotlar içinde kullanılabilir.
  • await, asenkron bir işlemi başlatmaz, yalnızca mevcut bir asenkron işlemin sonucunu bekler.

await Keyword'ünün Temel Kullanımı

  • await, bir Task veya Task<T> döndüren metotlar veya işlemler üzerinde kullanılır.
  • await ifadesi, kontrolü çağıran metoda geri vererek asenkron işlemin tamamlanmasını bekler.

Örnek: Basit Await Kullanımı

public async Task PerformOperationAsync()
{
await Task.Delay(1000); // 1 saniye bekleme
// Diğer asenkron işlemler
}

Bu örnekte, Task.Delay metodu bir saniye bekler ve await ifadesi, bu işlemin tamamlanmasını bekler.

Await İfadesinin Davranışı

  • await, asenkron işlemin tamamlanmasını beklerken kontrolü çağıran metoda geri verir.
  • Asenkron işlem tamamlandığında, await ifadesinden sonra gelen kod çalışmaya devam eder.
  • await ifadesi, kodun diğer işlemlerle (örneğin, UI güncellemeleri) kesintiye uğramadan devam etmesini sağlar.

ÖNEMLİ !!

Buradaki await keywordü ile bekleme denildiğinde , bir bekleme varsa bu nasıl asenkron oluyor ? sorusunu akıllara getirebiliyor. Fakat bu bekleme işlemi asenkron bir bekleme.

İşlemi yapan thread await keywordü gördüğü yerde metodun bitmesini beklemek yerine threadpool’a geri döner ve başka bir işlem için threadpoola bir talep geldiğinde o işlem için görevlendirilebilir. Bu sayede ilgili threadin, asenkron işlem süresince başka bir işlem yapmasını engellemiş olmaz ve thread üzerinde asenkronluk sağlanmış olur. İşlem sonunda await ile beklenen işlem sonrasında yeni bir threadle veya asenkron işlemi başlatan thread üzerinden kod akışı devam eder.

Örnek: Await Kullanımının Somutlaştırılması

public async Task<string> ReadFileAsync(string filePath)
{
using (StreamReader reader = new StreamReader(filePath))
{
// Asenkron dosya okuma işlemi başlatılır ve await ifadesi bu işlemin tamamlanmasını bekler.
string content = await reader.ReadToEndAsync();
// Dosya okuma işlemi tamamlandığında, bu kod çalışmaya devam eder.
return content;
}
}

Bu örnekte:

  1. reader.ReadToEndAsync() metodu, dosyanın içeriğini asenkron olarak okumak için çağrılır.
  2. await ifadesi, dosya okuma işleminin tamamlanmasını beklerken kontrolü çağıran metoda geri verir.
  3. Dosya okuma işlemi tamamlandığında, await ifadesinden sonra gelen kod çalışmaya devam eder ve dosya içeriği geriye döner.

Await Kullanımının Detayları

Asenkron İşlemlerin Kesintisiz Akışı:

  • await ifadesi, asenkron işlemler sırasında uygulamanın kesintiye uğramamasını sağlar.
  • Örneğin, bir UI uygulamasında, await ifadesi UI thread'ini bloklamadan işlemin tamamlanmasını bekler.

Hata Yönetimi:

  • await ifadesi ile kullanılan asenkron işlemler, try-catch blokları ile sarılarak hata yönetimi sağlanabilir.

Örnek: Await ile Hata Yönetimi

public async Task<string> FetchDataAsync(string url)
{
try
{
using (HttpClient client = new HttpClient())
{
// Asenkron web isteği başlatılır ve await ifadesi bu işlemin tamamlanmasını bekler.
string data = await client.GetStringAsync(url);
// İstek tamamlandığında, bu kod çalışmaya devam eder.
return data;
}
}
catch (Exception ex)
{
// Hata yönetimi burada gerçekleştirilir.
Console.WriteLine($"Hata: {ex.Message}");
return null;
}
}

Bu örnekte:

  1. client.GetStringAsync(url) metodu, asenkron bir web isteği başlatır.
  2. await ifadesi, web isteğinin tamamlanmasını bekler ve bu süre zarfında kontrolü çağıran metoda geri verir.
  3. Web isteği tamamlandığında, await ifadesinden sonra gelen kod çalışmaya devam eder. Yani çalışmak için await ile işaretlenen işlemin bitmesi beklenir.
  4. Eğer bir hata oluşursa, try-catch bloğu içinde hata yönetimi gerçekleştirilir.

Await ve Asenkron Akışın Anlaşılması

  • await ifadesi, asenkron işlemlerin senkronize olmuş gibi beklenmesini sağlar.
  • Bu, kodun daha okunabilir ve yönetilebilir olmasını sağlar.
  • await ifadesi, asenkron işlemlerin tamamlanmasını beklerken uygulamanın diğer işlemlerle kesintiye uğramadan çalışmasını sağlar.

Özet

C# dilinde await keyword'ü, asenkron işlemlerin tamamlanmasını beklemek için kullanılan güçlü bir araçtır. await ifadesi, asenkron işlemler sırasında kontrolü çağıran metoda geri vererek uygulamanın kesintisiz çalışmasını sağlar. await kullanımı, asenkron işlemleri daha okunabilir ve yönetilebilir hale getirir. await ifadesi ile birlikte hata yönetimi de kolaylıkla sağlanabilir. Asenkron işlemler tamamlandığında, await ifadesinden sonra gelen kod çalışmaya devam eder.

Async ve Await keywordleri ile ilgili genel kurallar ve dikkat edilmesi gerekenler

  1. async keyword’ü, metod imzasında dönüş türünden önce yer almalıdır.
  2. async ile işaretlenen metotlar Task, Task<T> veya void türünde değer dönmelidir.
  3. await keyword’ü yalnızca async işaretlenmiş bir metod içerisinde kullanılabilir.
  4. await keyword’ü Task veya Task<T> dönen işlemlerden önce kullanılmalıdır.
  5. async keyword’ü ile işaretlenen metotlar Task, Task<T> veya void türünde olmalıdır.
  1. Geriye Task ya da Task<T> döndüren metotların isimlerinin sonuna …Async eklemek yaygın bir konvansiyondur. Örneğin, GetDataAsync.
  2. async metotlarda kullananırken void metotlardan kaçınılmalı, yerine Task kullanılmalıdır.
  3. async kod yazarken çağrılan metotlar da async olmalıdır. Yani senkron metotları async metotlardan çağırmaktan kaçınılmalıdır (aksi takdirde await’in getirdiği avantajlar kaybedilebilir).
  4. async metotlarda kullanılan kaynaklar doğru bir şekilde serbest bırakılmalı, bu ihmal edilmemelidir. IAsyncDisposable.
  5. async ve await keywordleri yalnızca Task ve Task<T> döndüren metotlar içinde kullanılabilir. Bu yüzden constructor, destructor, property vs. de kullanılamaz.
  6. Constructor metot içerisinde asenkron işlemlerden kaçınılmalıdır. Çünkü constructor senkron çalışır ve aasenkron işlemleri desteklemez (zaten asenkron işlemler bir Task döndürdüğünden, constructor geri dönüş değeri olarak Task kullanamayacağı için bu mümkün değildir).

ConfigureAwait

  • async ve await keyword’leri ile kullanılan ConfigureAwait metodu, senkron bir işlemin tamamlanmasından sonra hangi bağlamda devam edileceğini belirlemeye yarayan bir metottur.
  • Yani ConfigureAwait metodu, asenkron bir metodu çağıran thread ve çalıştıran thread arasındaki ilişki üzerinde kontrol sağlamak için kullanılır.

.ConfigureAwait(false);

  • Devam eden kodun, content’ten bağımsız çalışmasını sağlar. Bu, özellikle performans iyileştirmesi ve deadlock’ları önlemek için kullanılır.

.ConfigureAwait(true);

  • Devam eden kodun, çağrıldığı context’te çalışmasını sağlar.
  • ConfigureAwait(false) kullanımı, context değiştirme maliyetini azaltarak performans iyileştirmesi sağlamaktadır. Özellikle UI uygulamalarında, UI thread’i boşta tutmak ve diğer thread’lerin verimli çalışması için tercih edilir.
  • Bunun dışında Asp.NET Core uygulamalarında, await’ten sonra devam eden kodun çağrıldığı context’te çalışması deadlock’a neden olabilir. Bu tip durumların önlenmesi için ConfigureAwait(false) kullanılır.

ConfigureAwait Kullanımı ile Context Değişimi Örneği

Bu örnekte, ConfigureAwait(false) ve ConfigureAwait(true) kullanımının thread context'ini nasıl etkilediğini göreceğiz. Öncelikle bir UI thread'indeymişiz gibi davranacağız ve farklı ConfigureAwait ayarlarının thread ID'lerini nasıl değiştirdiğini gözlemleyeceğiz.

Örnek Kod:

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static async Task Main(string[] args)
{
Console.WriteLine($"Main Thread ID: {Thread.CurrentThread.ManagedThreadId}");
await ExampleWithConfigureAwaitFalse();
await ExampleWithConfigureAwaitTrue();
}
static async Task ExampleWithConfigureAwaitFalse()
{
Console.WriteLine($"\\nExampleWithConfigureAwaitFalse - Initial Thread ID: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000).ConfigureAwait(false);
Console.WriteLine($"ExampleWithConfigureAwaitFalse - After ConfigureAwait(false) Thread ID: {Thread.CurrentThread.ManagedThreadId}");
}
static async Task ExampleWithConfigureAwaitTrue()
{
Console.WriteLine($"\\nExampleWithConfigureAwaitTrue - Initial Thread ID: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000).ConfigureAwait(true);
Console.WriteLine($"ExampleWithConfigureAwaitTrue - After ConfigureAwait(true) Thread ID: {Thread.CurrentThread.ManagedThreadId}");
}
}
//Main Thread ID: 1
//ExampleWithConfigureAwaitFalse - Initial Thread ID: 1
//ExampleWithConfigureAwaitFalse - After ConfigureAwait(false) Thread ID: 9
//ExampleWithConfigureAwaitTrue - Initial Thread ID: 9
//ExampleWithConfigureAwaitTrue - After ConfigureAwait(true) Thread ID: 9

Açıklama:

  • Main metodunda başlangıç thread ID'si yazdırılır.
  • ExampleWithConfigureAwaitFalse metodu çağrıldığında, Task.Delay(1000) asenkron işlemi ConfigureAwait(false) ile çalıştırılır. Bu, await sonrası işlemlerin farklı bir thread'de devam etmesine izin verir.
  • ExampleWithConfigureAwaitTrue metodu çağrıldığında, Task.Delay(1000) asenkron işlemi ConfigureAwait(true) ile çalıştırılır. Bu, await sonrası işlemlerin aynı context'te (aynı thread'de) devam etmesini sağlar.

Çıktıda, ConfigureAwait(false) kullanıldığında await sonrasında farklı bir thread ID'yi görebiliriz. ConfigureAwait(true) kullanıldığında ise aynı thread ID'sinin devam ettiğini görüyoruz.

Kaynak:Gençay Yıldız — Asenkron & Multithread Programlama

--

--