İlkay İlknur

just a developer...

C# 5.0 Async & Await Arka Planda Neler Oluyor ?

Merhaba Arkadaşlar,

Sizlerle şu ana kadar C# 5.0 ile beraber gelecek olan Asenkron Programlama yeniliklerini hangi noktalarda kullanabileceğimizi inceledik. Ancak bildiğiniz gibi her programlama dili yeniliği aslında arka planda yeni bir sihir yaratır. Daha önce kodumuzu neredeyse tamamen değiştirerek asenkron hale getirirken artık sadece 1-2 ufak değişiklikle kodumuzu asenkron bir şekilde çalışacak hale getirebiliyoruz. Peki gerçekten arka planda neler oluyor ? Compiler neler yapıyor da biz sadece 1-2 kod değişikliği ile kodumuzu asenkron hale getirebiliyoruz.

İlk olarak aşağıdaki gibi bir asenkron metodumuz olduğunu düşünelim.

static async Task<string> DownloadStringAsync(string Url)
{
 WebClient client = new WebClient();
 return await client.DownloadStringTaskAsync(Url);
}

Şimdi yazmış olduğumuz kodu derleyelim ve sonrasında Reflector yardımıyla açalım ve bakalım derleme sonucunda asenkron metodumuz compiler tarafından nasıl yeniden yazılmış ?

Dikkat : Yazının bu kısmından sonrası aşırı miktarda compiler tarafından üretilen kod içermektedir. Eğer compiler tarafından üretilen kodlara karşı alerjiniz varsa bu satırlar sizin için son çıkıştır. Ancak yazıyı buraya kadar okuduysanız sizinde aslında arka planda benim gibi neler olduğunu merak ediyor olduğunuzu düşünüyorum :)

private static Task<string> DownloadStringAsync(string Url) { <DownloadStringAsync>d__0 d__; d__ = new <DownloadStringAsync>d__0(0) { Url = Url, <>t__MoveNextDelegate = new Action(d__.MoveNext), $builder = AsyncTaskMethodBuilder<string>.Create() }; d__.MoveNext(); return d__.$builder.Task; }

Gördüğünüz gibi compiler, bizim yazmış olduğumuz DownloadStringAsync isimli metodu arka planda bir takım tipleri kullanarak yeniden yazdı.

Önceki yazılarımızdan da hatırlayacağımız üzere asenkron metotların dönüş tipi Task, Task<T> veya void olmak zorunda. Ancak bu dönüş tipini her zaman bizim yaratıp döndürmemiz gerekmemekte. Nitekim await kullanarak WebClient tipi içerisindeki DownloadStringTaskAsync metodunu çağırdığımızda bu metodun dönüş tipinin string olduğunu kolayca görebilmekteyiz. Ancak gördüğünüz gibi biz aslında asenkron metodumuzun dönüş tipini Task<string> olarak belirttik ve baktığımız zaman return ifadesinde de string döndürdük. Baktığımız zaman bu kullanım aslında tamamen legal. Ancak tabi ki bu işlem arka planda bir takım dönüşümler gerektirmekte. Bu dönüşümlerden ilki de Task<string> tipinin arka planda yaratılması ve metottan geri döndürülerek yazdığımız kodun arka planda da legalleştirilmesi. İşte compiler tarafından yazılan kodda görmüş olduğunuz AsyncTaskMethodBuilder tipi tam da bu işi gerçekleştirmekte.

public struct AsyncTaskMethodBuilder<TResult> { public Task<TResult> Task { get; } public void SetResult(TResult result); public void SetException(Exception exception); }

AsyncTaskMethodBuilder tipi gördüğünüz gibi içerisinde metot tarafından döndürülecek olan Task tipini ve metot işletilmesinden sonra da bu Task tipi içerisine sonucu yazmakta kullanılan SetResult isimli metodu içermekte. Bir de tabi eğer metot çalışması sırasında bir exception oluşursa bu exception’ı yine Task tipi içerisine yerleştirmek için bir de SetException metodunu bulundurmakta.

Compiler tarafından yeniden yazılan DownloadStringAsync isimli metodumuz içerisinde bir de <DownloadStringAsync>d__0 isimli bir tipin kullanıldığını görüyoruz. Bu tip te compiler tarafından yazılmış olan ve asenkron akışı tamamen içerisinde barındıran (enkapsüle eden) bir tip. Compiler tarafından yaratılmış olan bu tipin kodu ise şu şekilde.

[CompilerGenerated] private sealed class <DownloadStringAsync>d__0 { // Fields private bool $__disposing; public AsyncTaskMethodBuilder<string> $builder; private int <>1__state; public string <>3__Url; public Action <>t__MoveNextDelegate; private TaskAwaiter<string> <a1>t__$await3; public WebClient <client>5__1; public string Url; // Methods [DebuggerHidden] public <DownloadStringAsync>d__0(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] public void Dispose() { this.$__disposing = true; this.MoveNext(); this.<>1__state = -1; } public void MoveNext() { string <>t__result; try { string <1>t__$await2; bool $__doFinallyBodies = true; if (this.<>1__state != 1) { if (this.<>1__state != -1) { this.<client>5__1 = new WebClient(); this.<a1>t__$await3 = this.<client>5__1.DownloadStringTaskAsync(this.Url).GetAwaiter<string>(); if (this.<a1>t__$await3.IsCompleted) { goto Label_0089; } this.<>1__state = 1; $__doFinallyBodies = false; this.<a1>t__$await3.OnCompleted(this.<>t__MoveNextDelegate); } return; } this.<>1__state = 0; Label_0089: <1>t__$await2 = this.<a1>t__$await3.GetResult(); this.<a1>t__$await3 = new TaskAwaiter<string>(); <>t__result = <1>t__$await2; } catch (Exception <>t__ex) { this.<>1__state = -1; this.$builder.SetException(<>t__ex); return; } this.<>1__state = -1; this.$builder.SetResult(<>t__result); } }

Compiler tarafından yaratılan tipin constructor metoduna state isimli bir int değerin parametre olarak alındığını görüyoruz. Bu state değişkeni aslında bizim asenkron metot akışımızda önemli bir yere sahip. Bu yüzden <>1__state alanını aklımızın bir köşesinde tutalım ;)

Aslında sizin de göreceğiniz üzere <DownloadStringAsync>d__0 tipi içerisinde asenkron akışı yöneten metot MoveNext metodu. Bu metot içerisinde peki neler oluyor ?

Bu metodu incelemeden önce DownloadStringAsync isimli metodumuzun içerisine geri dönelim ve yapılan değer atamalarına bir bakalım.

private static Task<string> DownloadStringAsync(string Url) { <DownloadStringAsync>d__0 d__; d__ = new <DownloadStringAsync>d__0(0) { Url = Url, <>t__MoveNextDelegate = new Action(d__.MoveNext), $builder = AsyncTaskMethodBuilder<string>.Create() }; d__.MoveNext(); return d__.$builder.Task; }
  • İlk olarak constructor metoda 0 değeri parametre olarak geçilmiş. Bu değer de hatırlayacağımız üzere <DownloadStringAsync>d__0 tipi içerisindeki state değişkenine atanmakta.
  • <>t__MoveNextDelegate isimli delegate tipine de yine <DownloadStringAsync>d__0 tipi içerisindeki MoveNext metodu atanmakta.
  • Daha sonra ise yukarıda incelemiş olduğumuz AsyncTaskMethodBuilder tipinin Create isimli Factory metodu çağrılarak ilgili tipten bir nesne örneği $builder değişkenine atanmakta.
  • Son olarak ise nesne örneği yarattıktan sonra bu örnek üzerinden MoveNext metodu çağrılmakta.

Evet şimdi MoveNext metoduna geri dönüyoruz.

MoveNext metodu içerisinde ilk çağrım sırasında state değişkenimizin değeri 0. Bu nedenle metot içerisindeki 2 if ifadesinden de geçerek aşağıdaki kod bloğu işletilmekte.

if (this.<>1__state != 1) { if (this.<>1__state != -1) { this.<client>5__1 = new WebClient(); this.<a1>t__$await3 = this.<client>5__1.DownloadStringTaskAsync(this.Url).GetAwaiter<string>(); if (this.<a1>t__$await3.IsCompleted) { goto Label_0089; } this.<>1__state = 1; $__doFinallyBodies = false; this.<a1>t__$await3.OnCompleted(this.<>t__MoveNextDelegate); } return;

}

Çalışan koda baktığımızda bizim orjinal kodumuz içerisinde yazmış olduğumuz kodların bu kısımda olduğunu görmekteyiz. İlk olarak WebClient tipinden bir nesne örneği yaratılmakta ve akabinde de bu tip üzerinden DownloadStringTaskAsync isimli metot çağrılmakta.

Önceki yazılarımızda hatırlarsanız async & await kullanmamız için “awaitable pattern”’ı uygulamamız gerektiğinden sıkça bahsetmiştik. İşte awaitable pattern tam da kodumuzun bu noktasında devreye girmekte. Bir işlemin awaitable olması için aslında içerisinde GetAwaiter metodu ile aşağıdaki tipi döndürmesi gerekmekte.

public struct TaskAwaiter<TResult> { public bool IsCompleted { get; } public void OnCompleted(Action continuation); public TResult GetResult(); }

Bu tip aslında compiler tarafında oldukça önem arz etmekte. Bunun nedeni ise compiler arka planda yani yukarıdaki kodlarda bu tip üzerinden asenkron işlemin tamamlanıp tamamlanmadığını kontrol etmekte. Eğer işlem tamamlanmadıysa OnCompleted metodunu kullanarak bir callback ataması yapmakta. Tabi birde işlemin sonucunu elde edebilmek için GetResult metodunu çağırmakta.

TaskAwaiter tipi ile ilgili açıklamamızdan sonra tekrar MoveNext isimli metoda geri dönersek artık sanırım neden GetAwaiter metodunun çağrıldığını daha fazla açıklamamıza gerek yok. :)

GetAwaiter metodunun çağrılması sonrasında elde edilen TaskAwaiter tipi üzerinden ise bir takım kontroller yapılmakta. Bu kontrollerden ilki yapılan asenkron çağrımın sonlanıp sonlanmadığı ile ilgili. Eğer işlem sonlandıysa kod akışı doğrudan Label_0089 isimli alana goto ifadesi ile yönlenmekte. Eğer sonlamadıysa öncelikle state değişkeni 1’e çekilmekte daha sonra ise TaskAwaiter tipi içerisindeki OnCompleted metodu çağrılmakta ve callback olarak ta <DownloadStringAsync>d__0 tipi içerisindeki <>t__MoveNextDelegate alanı verilmekte. Yazımızın başlarında hatırlarsanız bu alana da MoveNext metodu atanmıştı !!! . Bu da demek oluyor ki asenkron işlem tamamlandıktan sonra yine MoveNext metodu çağırılıyor olacak. Sonrasında ise return ifadesi işletilerek MoveNext metodu sonlanmakta ve kontrol çağıran koda geri dönmekte yani bizim kodumuza. Bu şekilde de aslında bizim kullanıcı arayüzümüz cevap verebilir durumda olmakta.

Şimdi asenkron çağrım sonrasında işlemin sonlandığını ve TaskAwaiter tarafından OnCompleted metoduna parametre olarak geçirilen MoveNext metodunu işlemin sonlanmış olduğunu düşünerek tekrar işletelim. Bir üstteki paragrafta state değişkenine 1 değeri atanmıştı. Bu nedenle MoveNext metodu içerisindeki if kontrolleri girmeden doğrudan devam edecek ve ilk olarak state değişkenine 0 değeri atanacak ve sonrasında da Label_0089  ismiyle etiketlenmiş kod bloğu çalışacak. Bu kod bloğu içerisinde de TaskAwaiter tipi içerisindeki GetResult isimli metot kullanılarak asenkron metodun sonucu alınmakta.  Son olarak ise catch ifadesinin altında bulunan kısım işletilmekte ve state değişkenine –1 değeri atanmakta ve son olarakta AsyncTaskMethodBuilder tipi içerisindeki SetResult metodu kullanarak işlem sonucu AsyncTaskMethodBuilder tipi içerisindeki Task’ın içerisine yazılmakta.

MoveNext metoduna dikkat ettiyseniz bir de try-catch bloğu bulunmakta. Bu blok içerisinde de asenkron işlem sırasında oluşabilecek olan Exceptionlar yakalanmakta ve bir exception yakalanması durumunda state değişkeni –1’e çekilmekte ve oluşan exception da AsyncTaskMethodBuilder tipi içerisindeki SetException metodu çağrılarak AsyncTaskMethodBuilder tipinde bulunan Task’ın içerisindeki Exception alanına yazılmakta.

Genel yapıyı incelediğimizde aslında aklımıza C# dili içerisinde uzun süreden beri bulunan yield keywordünün alt yapısı gelmekte. Yield keywordü hatırlayacağınız üzere arka planda bir state machine yaratmaktaydı. C# 5.0 ile beraber gelen asenkron programlama yeniliklerinde de compiler aslında yine benzer bir yapıyı kullanarak bir state machine yaratmakta ve bu state machine içerisinde de callback atamalarını gerçekleştirmekte. Peki compiler neden bir state machine yaratmakta ? Aslında bunun durumu oldukça basit. Asenkron işlemin yönetimini kolay yapabilmek ve mevcutta bulunulan durumları saklayabilmek.

Özet olarak C# 5.0 ile gelen asenkron programlama yeniliklerinin arka planında callback tabanlı bir state machine yapısı yatmakta diyebiliriz.

Şu ana kadar Task tipinden bir nesne örneği döndüren asenkron metodun arka planını inceledik. Peki void dönüş tipine sahip olan asenkron metotlar nasıl yeniden yazılmakta ? Aslında çokta fazla bir değişiklik bulunmamakta. Bu yüzden de uzun uzadıya bir örnek yapıp incelememize gerek yok diye düşünüyorum. Aradaki tek fark AsyncTaskMethodBuilder<T> tipi yerine adından da anlayabileceğiniz üzere AsyncVoidMethodBuilder tipinin kullanımı olmakta. Compiler dönüş değeri void olduğu için herhangi bir Task tipi üretme işine girmemekte. :)

Compilerların arka olanda gerçekleştirdikleri işlemleri bilmek bazılarımıza gereksiz geliyor olsa da aslında bence oldukça önemli. Çünkü bir takım kullanımlar aslında baktığımızda bize legal olarak gözükmese de compiler arka planda bu programlama diline has özellikleri legal bir şekle sokmakta ve bu detayları da biliyor olmamız aslında bizim en doğru kullanımları yapmamızı sağlamakta. ;)

Umarım sizler için faydalı bir makale olmuştur.

Hoşçakalın,



Yorum Gönder