Makale Özeti

Microsoft hızla C#’ın bir sonraki sürümünü hazırlarken, geçtiğimiz güncelerde yeni sürüm hakkındaki bilgiler paylaşılmaya başlandı. Bu makalemde özellikle asenkron programlamayı hedefleyen yeni sürümde karşımıza çıkan async ve await anahtar kelimelerini tanıtarak, artık asenkron programlamanın nasıl kolaylaşacağını göreceğiz.

Makale

   13 şubat 2002 tarihinde 1.0 sürümü ile yazılım dünyasına katılan C#, bu tarihten sonraki her yeni sürümünde yazılım dünyasının en son trendlerini gerçekleştiren bir dil olma özelliğini sürdürmüştür.

   C# geliştiricileri her bir sürümde özellikle bir konuya odaklanarak adım adım ilerlemeyi tercih etmişlerdir. 1.0 sürümü ile kontrollü bir dil olarak ortaya çıkan C#, takip eden sürümlerde generikler, dil’e entegre sorgu (Linq), dinamik programlama gibi yazılım dünyasının önemli trendlerini başarıyla hayata gerçirmiştir.

   Günümüz modern uygulamalarında servis yönelimli mimari (SOA) giderek parlayan bir yıldız olmakta ve her geçen gün bu yönde geliştirilen uygulamalar artmakta. Bu mimariyi özellikle birbiriyle giderek daha entegre hale gelen iş dünyası uygulamalarında kolaylıkla görebiliriz. Bu mimarilerde, uygulamalar kendi kontrolleri dışında bulunan sistemlerle haberleştikleri, bu sistemlerden yanıt bekledikleri için standart kullanılan senkron programlama teknikleri malesef ki kullanıcılara uygulamada donmalar şeklinde yorumlanan anlık arayüz kilitlenmeleri olarak yansımakta. Harici sistemlerle haberleşmenin gerçekleştiği noktalarda uygulama geliştiricilerin tercih etmesi gereken asenkron programlama ise gerek iş mantığını değiştirmesi, gerekse de hayata geçirilmesindeki zorluklar nedeniyle maliyetli bir alternatif olarak karşımıza çıkmakta.

   Microsoft geliştiricileri de bu ihtiyacı görerek C#’ın yeni sürümünde bu yönde düzenlemeler yapmakta. Yakın zamanda daha fazla duymaya başlayacağınız yeni sürümde özellikle asenkron programlama tekniklerine yönelinerek yazılım geliştiricilerin hem daha az eforla hem de mevcut iş mantıklarında minimum değişiklikle uygulamalarında asenkron bir yapıya sahip olabilmeleri hedeflenmiş. Bu kapsamda Microsoft, C#’ın yeni sürümünde dile async ve await şeklinde iki yeni anahtar kelime daha eklemeyi planlamakta.

   Asenkron programlamaya yönelik yenilikleri heyecanla bekleyen yazılım geliştiriciler için; yeni özellikleri öğrenerek test edebileceğiniz bir Topluluk Teknik Önizlemesi (CTP – Community Technical Preview) olan Visual Studio Async CTP Visual Studio Asenkron programlama sitesinde bulunabilir. Visual Studio 2010 üzerine kurabileceğiniz bu CTP ile gelen örneklerle gerek C#, gerekse de Visual Basic.Net ile yeni gelen asenkron programlama özelliklerine göz atma şansı yakalayabilirsiniz.

   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/blog/";
    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/blog/";
    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/blog/";
    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/blog/";
                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;
        }
    }
}