C# 5, Async ile Kolaylaşan Asenkron İşlemleri

   PDC 2010 ardından yazdığım ve C#’ın bir sonraki sürümünde bizleri bekleyen yeni özellikleri sizlerle paylaştığım yazımda Visual Studio Async CTP bahsetmiştim. Visual Studio 2010 üzerine kurulan CTP paketi arka planda derleyiciyi de güncelleyerek yeni tanıştığımız asenkron anahtar kelimelerinin desteklenmesini sağlıyor. İsterseniz Visual Studio Async CTP ve devamında da .Net framework’ün yeni sürümünde  (5.0 ?) asenkron programlama konusunda bizleri bekleyen bu önemli değişikliği inceleyelim.

   Visual Studio Async CTP’yi indirip kurduğunuzda C#’a (ve Visual Basic.Net’e) eklenmiş iki yeni anahtar kelimeyi kullanmaya başlayabilirsiniz; async ve await. İlk denemelerinizde hemen farkedeceğiniz gibi bu anahtar kelimeler sadece .Net Framework 4 projelerinde kullanılabilir durumda. Bunun temel nedeni yeni asenkron özelliklerin arkaplanda .Net framework 4 ile birlikte tanıştığımız Task fonksyonalitesini kullanmasıdır. Derleyici, async ve await kullandığımız noktalara derleme-zamanında müdahale ederek kodumuzu asenkron bir yapıya dönüştürmektedir.

    Dile eklenen yeni anahtar kelimelerden async, kod içerisinde asenkron bir fonksiyon tanımlaması yapmamıza olanak vermektedir. async kullanımında dikkat edilmesi gereken önemli bir nokta; bu anahtar kelime ile işaretlenen fonksiyonların sadece void (boş), Task ya da Task<T> türünden bir geri dönüş tipine sahip olabilecekleridir.

   async anahtar kelimesini kullanarak tanımladığımız fonksiyonlarda, derleyecinin bizim adımıza müdahale ederek kodumuzu asenkron bir yapıya dönüştürebilmesi için uygun yerler belirtmemiz/işaretlememiz gerekecektir. Bu amaçla async dışında  await anahtar kelimesi de eklenmiştir. async ile tanımlanarak aseknron yapıda kullanılmasını istediğimiz bir fonksiyon içerisinde en az bir await ifadesi yer almalıdır; aksi takdirde derleyiciye fonksiyonu asenkron tanımla dediğimiz halde asenkron yapılabilecek bir işlem belirtmediğimizden mantıklı bir yapı ortaya çıkmayacaktır. await anahtar kelimesi ile ilgili düşülmesi gereken önemli bir notta; anahtar kelimenin sadece Task ya da Task<T> türünden değer dönen fonksiyonlar için kullanabiliyor olmasıdır. Bunun sebebi de yukarıda da bahsettiğim gibi tanıştığımız bu yeni anahtar kelimelerin aslında arka planda .Net framework 4 ile birlikte gelen Task fonksiyonalitesini kullanmasıdır.

    Dile eklenen bu iki anahtar kelime dışında, yazılım geliştiricilerin yeni fonksiyonliteleri hızlıca kullanabilmesi adına, mevcut pek çok kütüphaneye de eklemeler yapılmış durumda. Bu eklemeler için aklıma gelen ilk örnek ise WebClient sınıfına eklenen DownloadStringTaskAsync fonksiyonudur.

    Bu noktada akıllara şu soru gelebilir; Visual Studio Async CTP ile birlikte bu eklemelerin olduğu tüm kütüphanelerin yeni sürümleri mi geldi? Daha da önemlisi istemci tarafında .Net framework’ü mü değişiyor? Yeni bir sürüm mü kurmalıyım? Hemen içinizi rahatlatayım; CTP ile birlikte ne bu kütüphanelerin yeni sürümleri geldi, ne de istemciye yeni bir framework kurulumu yapılması gerekli. Yeni kütüphane sürümleri yok, peki yeni özellikleri nasıl kullabiliyorum? Bu noktada geliştiriciler oldukça akıllıca bir işe imza atarak mevcut kütüphaneleri değiştirmek yerine bu kütüphanelerdeki sınıflara genişletme (extension) fonksiyonları yardımıyla yeni özellikleri katmayı seçmişler. Yapmanız gereken sadace projenizde Visual Studio Async CTP ile birlikte gelen AsyncCtpLibrary.dll assembly’sine referans göstermek. Bu assembly CTP kurulumu sırasında GAC’a kayıt edilmediği için referans olarak "%userprofile%\My Documents\Microsoft Visual Studio Async CTP\Samples" klasörü içerisindeki assembly’yi kullanmalısınız. Refaransı eklemeniz sonrasında yeni eklenen asenkron fonksiyonları kullanabilirsiniz. .Net framework’ün bir sonraki sürümünde bu fonksiyonaliteler ilgili sınıfların içerisinde yeralacağından bu şekilde bir kullanıma ihtiyaç kalmayacaktır.

   Sanırım şimdilik Visual Studio Async CTP hakkında bu kadar teorik bilgi yeterli olacaktır. Konunun pekişmesi adına yazımın geri kalanında, biz yazılımcıların en fazla tercih ettiği şekilde, örnek bir kod üzerinde devam etmek daha iyi olacaktır.

   Aşağıda, bu konu hakkındaki en basit (ve sanırım en yaygın) örneklerden birisini bulabilirsiniz. Örnekte; arayüzde yer alan bir butona basılmasıyla kullanıcı tarafından tetiklenen bir fonksiyon yer almakta. Fonksiyon, oluşturduğu bir WebClient örneği üzerinden web sayfası içeriğini string olarak almakta, ardından da içerisindeki url’ler bulunarak adresListesi (listbox) üzerinde listelemekte.

private void adresleriBul_Click(object sender, EventArgs e) {
    var adress = "http://www.enterprisecoding.com/";
    var icerik = new WebClient().DownloadString(adress);
    var eslesimler = Regex.Matches(icerik,
                        @"(?<Protocol>\w+):\/\/(?<Domain>[\w@][\w.:@]+)\/?[\w\.?=%&=\-@/$,]*",
                        RegexOptions.IgnoreCase);

    foreach (Match eslesim in eslesimler) {
        adresListesi.Items.Add(eslesim.Value);
    }
}

   Uygulamanın çalıştığı sistemin ve web uygulamasının bulunduğu karşı sistemin hızlı bir internet bağlantısı olması durumunda bu kodda kullanıcıyı etkileyen herhangi bir durum oluşmayacaktır; ki bu iyi bir sanaryodur. Kötü senaryoda ise (büyük olasılıkla sahada olacak olan da bu senaryodur), sistemlerden en az birisinde yoğunluk/yavaşlama olacaktır; bu durumda da karşı uygulamadan yanıt alınana kadar kendi uygulamanızın arayüzü yanıt veremecek ve bu durumda kullanıcı/müşteri tarafından “kötü uygulama”, “donmalar oluyor” v.b. şekillerde olumsuz yorumlanacaktır.

   Karşı sistemdeki yavaşlık sırasında uygulama arayüzünüzün yanıt veremiyor olmasının nedeni yukarıdaki fonksiyonun arayüz ile aynı thread üzerinde çalışıyor olmasıdır. Uygulamanız bu fonksiyondan çıkmadığı sürece arayüz işlemleri yapılamayacağından ekranı taşıma, tazeleme v.b. işlemler yapılamayacaktır. Problemin çözümü de işi ayrı bir thread üzerine taşıyarak uygulama ana thread’ini meşgul etmeyerek arayüzün kendi işlevlerini yerine getirmesine devam etmesini sağlamaktır; fakat bu iş her zaman göründüğü kadar kolay olmayacaktır.

   Yukarıdaki örnek kodumuzu thread’li olarak yeniden düzenleyecek olursak yönetmemiz gereken pek çok yeni iş olacaktır. Thread’lerin başlaması-bitmesi, hataların yönetilmesi ve hatta eşleşen adreslerin adresListesine eklenmesi noktasında arayüz thread’i ile yeniden senkronize olmak gerekecektir. Basit uygulamalarda bu işlemler fazla iş yükü oluşturmasa da büyük kurumsal uygulamalarda, özellikle de harici sistemlerle sık sık iletişim kuruyorlarsa, önemli bir iş yüküne neden olacaktır. Aşağıda uygulamanın thread kullanılarak yazılmış bir versiyonunu bulabilirsiniz;

delegate void EslesimleriEkleCallback(MatchCollection eslesimler);

private void adresleriBul_Click(object sender, EventArgs e) {
    var islem = new Thread(new ThreadStart(AdressleriBul));

    islem.Start();
}

private void AdressleriBul() {
    var adress = "http://www.enterprisecoding.com/";
    var icerik = new WebClient().DownloadString(adress);
    var eslesimler = Regex.Matches(icerik, 
                        @"(?<Protocol>\w+):\/\/(?<Domain>[\w@][\w.:@]+)\/?[\w\.?=%&=\-@/$,]*", 
                        RegexOptions.IgnoreCase);

    if (adresListesi.InvokeRequired) {//Fonksiyon ayrı thread’den çağırılmış
        var callback = new EslesimleriEkleCallback(EslesimleriEkle);
        Invoke(callback, new[] { eslesimler });
    }
    else { //Aynı thread, doğrudan çağırılabilir
        EslesimleriEkle(eslesimler);
    }

            
}

private void EslesimleriEkle(MatchCollection eslesimler) {
    foreach (Match eslesim in eslesimler) {
        adresListesi.Items.Add(eslesim.Value);
    }
}

   Tam da bu noktada, yönetilmesi ve kontrol edilmesi gereken pek çok iş ve giderek karmaşıklaşan bir kod oluşmaya başlarken, Visual Studio Async CTP ve beraberinde gelen async ve await anahtar kelimelerin kullanımı ile yukarıdaki kodumuz kolaylıkla aşağıdaki gibi asenkron çalışmaya başlayacaktır;

private async void adresleriBul_Click(object sender, EventArgs e) {
    var adress = "http://www.enterprisecoding.com/";
    var icerik = await new WebClient().DownloadStringTaskAsync(adress);
    var eslesimler = Regex.Matches(icerik,
                        @"(?<Protocol>\w+):\/\/(?<Domain>[\w@][\w.:@]+)\/?[\w\.?=%&=\-@/$,]*",
                        RegexOptions.IgnoreCase);

    foreach (Match eslesim in eslesimler) {
        adresListesi.Items.Add(eslesim.Value);
    }
}

   Yukarıdaki kodu bir ilk hali ile kıyaslayacak olursak sadece ve sadece 3 noktada değişiklik olduğunu görebiliriz. Karmaşık kodlama mantıkları yok, senkronize edilmesi/yönetilmesi gereken threadler yok, daha  da önemlisi spagetti kod yok. Kodumuzun birinci satırında fonksiyonumuzu asenkron olarak işaretliyoruz, 3. satırında işlemin asenkron olarak bekleneceğini belirtiyoruz, son olarak da yine 3. satırında WebClient sınıfı için Visual Studio Async CTP içerisinde yazılmış olan extension fonksiyonu (DownloadStringTaskAsync) kullanılıyoruz. DownloadStringTaskAsync fonksiyonu geriye Task<string> türünden bir değer dönmekte ve derleme esnasında bu yapı bizim için derleyici tarafından asenkron olarak yeniden yazılacaktır. Üstelik tüm bunlar yapılırken siz arkaplandaki thread senkronizasyonu v.b. işlemler üzerinde de zaman harcamıyorsunuz; örneğin normalde yukarıdaki kodumuzda adresListesi’ne değerleri eklerken öncelikle ana threadimizle senkron olmamız gerekiyorken Async ile birlikte bunlar bizim adımıza yapılıyor. Harika değil mi!

  Derleyicinin bizim için kodumuzu nasıl değiştirdiğini ise uygulamamızı reflector ile açarak görebiliriz;

private void adresleriBul_Click(object sender, EventArgs e) {
    <adresleriBul_Click>d__0 d__ = new <adresleriBul_Click>d__0(0);
    d__.<>4__this = this;
    d__.sender = sender;
    d__.e = e;
    d__.MoveNextDelegate = new Action(d__.MoveNext);
    d__.$builder = VoidAsyncMethodBuilder.Create();
    d__.MoveNext();
}

    Durun bir dakika bu bizim yazdığımız kod değil! 🙂

   Derleyici oldukça değiştirmiş değil mi! Peki bu koddaki d__0 sınıfı nedir? Aşağıda bulabileceğiniz bu sınıfı da görerek derleyicinin iş mantığımızı nasıl yorumladığını daha rahat görebilirsiniz:

[CompilerGenerated]
private sealed class <adresleriBul_Click>d__0 {
    private bool $__disposing;
    private bool $__doFinallyBodies;
    public VoidAsyncMethodBuilder $builder;
    private int <>1__state;
    public EventArgs <>3__e;
    public object <>3__sender;
    public AnaEkran <>4__this;
    private string <1>t__$await5;
    private TaskAwaiter<string> <a1>t__$await6;
    public string <adress>5__1;
    public Match <eslesim>5__4;
    public MatchCollection <eslesimler>5__3;
    public string <icerik>5__2;
    public EventArgs e;
    public Action MoveNextDelegate;
    public object sender;

    [DebuggerHidden]
    public <adresleriBul_Click>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() {
        try {
            this.$__doFinallyBodies = true;
            if (this.<>1__state != 1) {
                if (this.<>1__state == -1) {
                    return;
                }

                this.<adress>5__1 = "http://www.enterprisecoding.com/";
                this.<a1>t__$await6 = new WebClient().DownloadStringTaskAsync(this.<adress>5__1).GetAwaiter<string>();
                this.<>1__state = 1;
                this.$__doFinallyBodies = false;
                if (this.<a1>t__$await6.BeginAwait(this.MoveNextDelegate)) {
                    return;
                }
                this.$__doFinallyBodies = true;
            }

            this.<>1__state = 0;
            this.<1>t__$await5 = this.<a1>t__$await6.EndAwait();
            this.<icerik>5__2 = this.<1>t__$await5;
            this.<eslesimler>5__3 = Regex.Matches(this.<icerik>5__2, 
                           @"(?<Protocol>\w+):\/\/(?<Domain>[\w@][\w.:@]+)\/?[\w\.?=%&=\-@/$,]*", 
   RegexOptions.IgnoreCase);
            IEnumerator CS$5$0002 = this.<eslesimler>5__3.GetEnumerator();
            try {
                while (CS$5$0002.MoveNext()) {
                    this.<eslesim>5__4 = (Match) CS$5$0002.Current;
                    this.<>4__this.adresListesi.Items.Add(this.<eslesim>5__4.Value);
                }
            }
            finally {
                if (this.$__doFinallyBodies) {
                    IDisposable CS$0$0003 = CS$5$0002 as IDisposable;
                    if (CS$0$0003 != null) {
                        CS$0$0003.Dispose();
                    }
                }
            }
            this.<>1__state = -1;
            this.$builder.SetCompleted();
        }
        catch (Exception) {
            this.<>1__state = -1;
            this.$builder.SetCompleted();
            throw;
        }
    }
}

Fatih Boy

Ankara'da yaşayan Fatih, bir kamu kurumunda danışman olarak çalışmaktadır. ALM süreçleri, kurumsal veri yolu sistemleri, kurumsal altyapı ve yazılım geliştirme konularında destek vermektedir. Boş zamanlarında açık kaynak kodlu projeler geliştirmeyi ve bilgisini yazdığı makalelerle paylaşmayı seven Fatih, aynı zamanda Visual C# ve Visual Studio teknolojileri konusundan Microsoft tarafından altı yıl üst üste MVP (En Değerli Profesyonel) ödülüne layık görülmüştür. İş hayatı boyunca masaüstü uygulamaları, web teknolojileri, akıllı istemciler gibi konularda Asp.Net, Php, C#, Java programlama dilleri ve MySql, MsSql ve Oracle gibi veritabanı yönetim yazılımları ile çalışmıştır. İngilizce ve Türkçe olarak yayınlanan makalelerini gerek İngilizce bloğunda, gerekse de Türkçe bloğunda bulabileceğiniz gibi web sitesinden de açık kaynak kodlu geliştirdiği yazılımlarına ulaşabilirsiniz. vCard - Twitter - Facebook - Google+

2 yorum

  1. Abdurrahman Güngör   •  

    Kesinlikle beklediğim bir özellikti. Çok sevindirici ve harika bir gelişme.

  2. Mustafa   •  

    Fatih bey, çok açıklayıcı ve anlaşılır bir yazı olmuş. Teşekkürler.

Bir Cevap Yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir