C Sharp Programlama Dili/Asenkron programlama

Bu bölümde, yazdığımız Windows programının cevap verebilirliğini artıran bir yöntem göreceğiz. Bu yöntemde işlemci bir çevre bileşenine bir emir verir, ancak bu emrin yerine getirilmesini beklemez, kendi işine devam eder. Çevre birimi ilgili işi tamamladığında bir interrupt ile bunu işlemciye bildirir. Fark ettiğiniz üzere burada tek thread çalışmaktadır. Aslında asenkron programlama ile yaptığımız şeyi birden fazla thread açarak da yapabiliriz. Ancak asenkron programlama yöntemi bütün işi tek bir thread ile hallettiği için birden fazla thread kullanma yaklaşımına göre daha verimlidir.

Bir Windows form uygulaması programın kesildiği yerde donar. Buradaki kesilmekten kastımız işlemcinin bir metottaki bir satırda bir süre takılıp kalması, ilerlememesidir. Bu takılmaların çoğunda işlemci başka bir çevre biriminin iş yapmasını bekler, ancak bazen de yoğun işlemci gücü gerektiren işlemler yapılabilir. Asenkron programlama yaklaşımı birinci durumda kullanılabilir. İkinci durumda programın cevap verebilirliğini sağlamak için yeni thread açılması kaçınılmazdır.

Bir Windows form uygulamasının donması kullanıcı deneyimi açısından çok kötü bir şeydir. Formun üzerindeki kontroller tepki vermez hale gelir, hatta program penceresinin sağ üst kısmındaki kapatma, simge durumuna küçültme ve ekranı kaplama düğmeleri de çalışmaz hale gelir. Böyle bir durumda çoğu kullanıcı programın kilitlendiğini sanarak CTRL+ALT+DEL kısayolunu kullanarak programı kapatır.

Form tabanlı uygulamalarda program akışının kesilmesi formun donmasına neden olur. Çünkü formlar mesaj döngüsü mantığında çalışır. Bir formda sürekli sonsuz bir while döngüsünde dönülür. Bu while döngüsünün içinde formun kullanıcı eylemlerine tepki vermesini sağlayan kodlar bulunur, örneğin fare işaretçisinin bir butonun üzerine getirildiğinde butonun renk değiştirmesi gibi. Ayrıca form formun üzerindeki kontrollerin özelliklerine göre sürekli güncellenir. Örneğin formun üzerindeki bir butonun üzerinde bir metni değiştirmişsek bu değişiklik while döngüsünün sıradaki ilk iterasyonunda görüntüye yansıtılır. Son olarak bir metot çağrısı yaptığımızda da bu döngü yarıda kalır, metodun işlemleri yapılır, sonra kalınan yerden döngüye geri dönülür. İşte işlemi uzun süren bir metot çağrısı yaptığımızda formun donmasının sebebi budur. Ayrıca işletim sistemi belirli aralıklarla forma formun hayatta olup olmadığını öğrenmek için mesaj gönderir. Form da hayatta olduğuna ilişkin mesajı göndererek cevap verir. Bu işlem de mesaj döngüsünün içinde yapılır. Programımız bir metot içinde uzun süre kalırsa işletim sistemi bu mesajlara cevap alamaz ve programı "cevap vermiyor" (not responding) olarak işaretler. Windows, cevap vermeyen uygulamaların pencerelerini ilgili pencereyle aynı boyutlarda bir hayalet pencereyle değiştirir.

Asenkron metotlar değiştir

C# 5'ten itibaren asenkron işlemler asenkron metotlarla yapılır. Bundan öncesinde asenkron işlemler callback fonksiyonlarıyla ve aynı işi birden fazla metoda bölerek yapılabiliyordu. Ancak bu yöntem anlaşılmaz, zor ve hata yapmaya açıktı. Derleyici çoğu işi programcıya bırakıyordu. Ancak asenkron metot yaklaşımında asenkron işlemler hemen hemen senkron işlemler gibi yapılır, derleyici işin zor kısmını kendi halleder. Aşağıda bir asenkron metot bildirimi bulunmaktadır:

async Task<int> WebeErisAsync()  
{
    //HttpClient System.Net.Http isim alanında bulunan bir sınıftır.
    HttpClient hc = new HttpClient();
    Task<string> ts = hc.GetStringAsync("https://msdn.microsoft.com");   
    BaskaIs();    
    string s = await ts;    
    return s.Length;  
}

Asenkron metotlar async belirteciyle işaretlenir. async belirteciyle işaretlenen metotlar await anahtar sözcüğünü içerebilir, ancak içermesi zorunlu değildir. await anahtar sözcüğü geriye Task veya Task<T> döndüren bir ifadeyle kullanılmalıdır. Task ve Task<T> System.Threading.Tasks isim alanında bulunan birer sınıftır. Task sınıfı geriye değer döndürmeyen işleri, Task<T> sınıfı ise geriye değer döndüren işleri temsil eder. Şablon tip geriye döndürülecek nesnenin tipini belirtir. await anahtar sözcüğüyle kullanılan ifadenin temsil ettiği iş tamamlandıysa -varsa- ilgili atama yapılır. İş tamamlanmadıysa tamamlandığında geri dönülmek üzere ilgili metodu çağıran metoda geri dönülür. Yukarıdaki örnekte

Task<string> ts = hc.GetStringAsync("https://msdn.microsoft.com");

ifadesiyle hem bir web sayfasını oluşturan metinlerin string olarak çekilmesi işlemine başlanır hem de geriye bir Task<string> nesnesi döndürülür. Burada önemli olan ilgili Task<string> nesnesinin oluşturulması için işin bitmesinin beklenmemesidir. İlgili Task<string> nesnesi ilgili işin gidişatı hakkında bilgi verir.

BaskaIs();

Burada ilgili Task<string> nesnesinin üreteceği değerle ilgili olmayan kodlar çalıştırılır.

string s = await ts;

Bu satır, ilgili Task<string> nesnesinin temsil ettiği işi bekleme komutudur. Eğer ilgili iş bitmişse elde edilen string s değişkenine atanır. Eğer ilgili iş bitmemişse metodu çağıran metoda geri dönülür. Eğer Task<string> nesnesinin oluşturulduğu satırla bu işlemin sonucunun beklendiği await'li satır arasında yapılacak başka iş yoksa bu iki satır aşağıdaki gibi tek satır olacak şekilde birleştirilebilir:

string s = await hc.GetStringAsync("https://msdn.microsoft.com");

Geleneksel olarak asenkron metotların "Async" takısıyla bitmesi tavsiye edilir. Ancak zorunlu değildir. Async takısını gören başka programcılar bu metodun bir asenkron metot olduğunu anlarlar ve await anahtar sözcüğüyle kullanırlar.

Asenkron metotların çeşitli geri dönüş tipleri olabilir:

  • Task<T> İlgili asenkron metot geriye değer döndürüyorsa bu tip kullanılır. Metodun geriye döndürdüğü değerin tipine bağlı olarak bu şablon sınıfın kurulmuş hali kullanılmalıdır.
  • Task İlgili asenkron metot geriye değer döndürmüyorsa bu tip kullanılır.
  • void Sadece event handler metotlarda kullanılır. Event handler metot demek bilinçli olarak çağrılmayan, sadece bir olayın tetiklenmesiyle çalışan metot demektir.
  • C# 7'den itibaren GetAwaiter() metodunu içeren herhangi bir tiple asenkron metotlar geri dönebilir.

Asenkron metotlarda dolaşım değiştir

Bir asenkron metodu çağıran metodun da asenkron olması gerekir. Çünkü bir asenkron metot bir noktada kesildiğinde o metodu çağıran metot da kesilecektir. Bu kesinti zinciri çağrılan metottan çağıran metoda doğru ilerler. Bu ilerleyişin son bulduğu nokta event handler metottur. Çünkü event handler metotlar bilinçli bir metot çağrısıyla değil, olayın tetiklenmesiyle çalışır. Olayın tetiklendiği metodun asenkron olmasına gerek yoktur. Şimdi isterseniz birbirini çağıran metotlar üzerinde await anahtar sözcüğünün akışı nasıl yönlendirdiğini görelim:

async void EventHandler()
{
    Task<string> t = Metot1(); //1. satır
    Console.WriteLine("EventHandler() metodu - 1. nokta"); //2. satır
    string s = await t; //3. satır
    Console.WriteLine("EventHandler() metodu - 2. nokta"); //4. satır
}
async Task<string> Metot1()
{
    Task<int> t = Metot2(); //1. satır
    Console.WriteLine("Metot1() metodu - 1. nokta"); //2. satır
    int i = await t; //3. satır
    Console.WriteLine("Metot1() metodu - 2. nokta"); //4. satır
    return i.ToString(); //5. satır
}
async Task<int> Metot2()
{
    Task t = Metot3(); //1. satır
    Console.WriteLine("Metot2() metodu - 1. nokta"); //2. satır
    await t; //3. satır
    Console.WriteLine("Metot2() metodu - 2. nokta"); //4. satır
    return 0; //5. satır
}
async Task Metot3()
{
    Task t = Task.Delay(5000); //5 saniye beklemek için. 1. satır.
    Console.WriteLine("Metot3() metodu - 1. nokta"); //2. satır
    await t; //3. satır
    Console.WriteLine("Metot3() metodu - 2. nokta"); //4. satır
}

Task sınıfına ait statik Delay() metodu programlardaki kesintiye neden olan durumları simüle etmek için kullanılır, programın akışını belirli bir süre kesintiye uğratır. Programın adım adım akışı şu şekildedir:

  1. EventHandler metodunda 1. satır çalıştırılır. Burada Metot1() metoduna dallanma yapılır. Bu işi temsil eden Task<string> nesnesi oluşturulur.
  2. Metot1() metodunda 1. satır çalıştırılır. Burada Metot2() metoduna dallanma yapılır. Bu işi temsil eden Task<int> nesnesi oluşturulur.
  3. Metot2() metodunda 1. satır çalıştırılır. Burada Metot3() metoduna dallanma yapılır. Bu işi temsil eden Task nesnesi oluşturulur.
  4. Metot3() metodunda 1. satır çalıştırılır. Task.Delay() metodu kesintiye neden olan asenkron bir metottur. Ancak program kesintiye uğramaz. Metot3() metodunun 1. noktasının çıktısını veren komut çalıştırılır (2. satır). Daha sonra 3. satıra bakılır. 3. satırda bir await ifadesi vardır ve t değişkeniyle kullanılmıştır. Bu satır programa şunu der: "t değişkeninin temsil ettiği görev bittiyse ilerleyebilirsin, ancak t değişkeninin temsil ettiği görev bitmediyse seni çağıran metoda geri dönmelisin, t değişkeninin temsil ettiği görev bittiğinde seni haberdar edeceğim." Henüz 5 saniye dolmamıştır, dolayısıyla t değişkeninin temsil ettiği görev bitmemiştir. Dolayısıyla akış Metot2() metodunun 2. satırına döner.
  5. Metot2() metodunun 2. satırında ekrana "Metot2() metodu - 1. nokta" çıktısı verilir. 3 satırda t değişkeni kontrol edilir. Henüz Metot3() işlemin bittiğine dair mesajı göndermemiştir. O yüzden akış Metot1() metodunun 2. satırına gelir.
  6. Metot1() metodunun 2. satırında ekrana "Metot1() metodu - 1. nokta" yazılır. 3. satırda henüz t değişkeninin temsil ettiği görev tamamlanmadığı için EventHandler() metodunun 2. satırına gidilir. Burada ekrana "EventHandler() metodu - 1. nokta" çıktısı verilir. 3. satırda henüz t değişkeninin temsil ettiği görev tamamlanmamıştır. Bunun üzerine eğer üzerinde çalıştığımız program bir form tabanlı uygulama ise formun mesaj döngüsüne dönülür. Eğer üzerinde çalıştığımız program bir konsol uygulaması ise olayı tetikleyen metoda dönülür (örneğin Main() metoduna). Main metodunda akışın kitlenmesine neden olacak bir komut yoksa olayın tetiklendiği satırdan sonraki komutlar çalıştırılır ve program sonlandırılır. Programdan çıkılırken beklenen herhangi bir sinyal olup olmadığına bakılmaz.
  7. Form tabanlı uygulamada EventHandler() metodunun içindeki await'ten sinyal geldiğinde mesaj döngüsünden çıkılır ve EventHandler() metodunun 3. satırına gelinir.
  8. EventHandler() metodunun 3. satırı bizi Metot1() metodunun 3. satırına yönlendirir.
  9. Metot1() metodunun 3. satırı bizi Metot2() metodunun 3. satırına yönlendirir.
  10. Metot2() metodunun 3. satırı bizi Metot3() metodunun 3. satırına yönlendirir.
  11. Metot3() metodunun 3. satırının işlemi bitmiştir. Akış 3. metodun 4. satırından devam eder ve ekrana "Metot3() metodu - 2. nokta" yazılır.
  12. Metot3() metodu bittiğine göre sıra Metot2() metodunun 4. satırına gelinir (3. satırda herhangi bir şey döndürülmemekte, sadece işlemin bittiği belirtilmektedir). 4. satırda ekrana "Metot2() metodu - 2. nokta" yazılır. Metot2() metodu geriye 0 değerini döndürmektedir.
  13. Metot2() metodu bittiğine göre sıra Metot1() metodunun 3. satırına gelmiştir. Metot1() metodunun 3. satırında Metot2() metodunun geri döndürdüğü 0 değeri i değişkenine atanır. 4. satırda ekrana "Metot1() metodu - 2. nokta" yazılır. Metot1() metodu geriye "0" stringini döndürür.
  14. Metot1() metodu bittiğine göre EventHandler() metodunun 3. satırına dönülür. Metot1() metodunun geri döndürdüğü "0" stringi s değişkenine atanır. 4. satırda ekrana "EventHandler() metodu - 2. nokta" yazılır.
  15. Bütün metotların işi bittiğine göre mesaj döngüsüne geri dönülür.

Elbetteki form tabanlı uygulamalarda Console sınıfıyla ekrana çıktı veremezsiniz. Form tabanlı uygulama geliştirmişseniz Console sınıfı metotlarını ListBox'a ekleme yapan komutlarla değiştirebilirsiniz.

Son tahlilde ekrana verilen (veya ListBox'a eklenen) tüm çıktılar şöyle olur:

Metot3() metodu - 1. nokta
Metot2() metodu - 1. nokta
Metot1() metodu - 1. nokta
EventHandler() metodu - 1. nokta
Metot3() metodu - 2. nokta
Metot2() metodu - 2. nokta
Metot1() metodu - 2. nokta
Eventhandler() metodu - 2. nokta

NOT: Aslında her bir await kendini çağıran metodun await'ine mesaj gönderir. EventHandler() metodunun gönderdiği sinyal ise mesaj döngüsünedir.

NOT: Asenkron bir metotta parametreler ref veya out olarak işaretlenemez, referansla değer döndüremez.

Birden fazla görevle çalışmak değiştir

Tek bir görevle çalışabileceğimiz gibi bir görev dizisi veya koleksiyonuyla da çalışabiliriz. Bu durumda await ifadesini bir görev dizisi veya koleksiyonuyla kullanabiliriz. Toplu görev işlemlerinde görevlerin tümünün veya herhangi birisinin gerçekleşmesini işlemin tamamlanması için yeterli sayabiliriz. Örnek:

//Bu bir Windows form uygulaması
//Forma button isimli bir buton, bir de listbox isimli bir ListBox ekleyin.
//using System.Net.Http; direktifini Form1 sınıfının başına ekleyin.
async Task<int> WebSitesininBoyutunuAlAsync(string site)
{
    HttpClient hc=new HttpClient();
    byte[] icerik = await hc.GetByteArrayAsync(site);
    return icerik.Length;
}
//Bu, butonun Click olayına bağlanan event handler metodu.
async void button_Click(object sender,EventArgs e)
{
    List<string> siteler = new List<string>()
    {
        "https://msdn.microsoft.com",  
        "https://msdn.microsoft.com/library/windows/apps/br211380.aspx",  
        "https://msdn.microsoft.com/library/hh290136.aspx",  
        "https://msdn.microsoft.com/library/ee256749.aspx",  
        "https://msdn.microsoft.com/library/hh290138.aspx",  
        "https://msdn.microsoft.com/library/hh290140.aspx",  
        "https://msdn.microsoft.com/library/dd470362.aspx",  
        "https://msdn.microsoft.com/library/aa578028.aspx",  
        "https://msdn.microsoft.com/library/ms404677.aspx",  
        "https://msdn.microsoft.com/library/ff730837.aspx"
    };
    IEnumerable<Task<int>> gorevler = from site in siteler select WebSitesininBoyutunuAlAsync(site);
    Task<int>[] gorevDizisi = gorevler.ToArray(); //Burası indirmelerin başladığı satır
    int[] boyutlar = await Task.WhenAll(gorevDizisi);
    listbox.DataSource = boyutlar;
}

Burada 10 tane web sitesinin bayt cinsinden boyutları listbox'a yazılmaktadır. Task.WhenAll() metodu parametre olarak bir görev dizisi alır. Dizideki bütün görevler tamamlandığında await ifadesinden ileriye gidilmesine izin verir. Her bir görevin sonucu boyutlar dizisinin bir elemanına yazılır. Bu programda listbox bir süre boş kalır, sonra birden listbox'taki 10 satır da dolar. Şimdi sadece event handler metodunu şötle değiştirelim:

async void button_Click(object sender,EventArgs e)
{
    List<string> siteler = new List<string>()
    {
        "https://msdn.microsoft.com",  
        "https://msdn.microsoft.com/library/windows/apps/br211380.aspx",  
        "https://msdn.microsoft.com/library/hh290136.aspx",  
        "https://msdn.microsoft.com/library/ee256749.aspx",  
        "https://msdn.microsoft.com/library/hh290138.aspx",  
        "https://msdn.microsoft.com/library/hh290140.aspx",  
        "https://msdn.microsoft.com/library/dd470362.aspx",  
        "https://msdn.microsoft.com/library/aa578028.aspx",  
        "https://msdn.microsoft.com/library/ms404677.aspx",  
        "https://msdn.microsoft.com/library/ff730837.aspx"
    };
    IEnumerable<Task<int>> gorevler = from site in siteler select WebSitesininBoyutunuAlAsync(site);
    List<Task<int>> gorevKol = gorevler.ToList(); //Burası indirmelerin başladığı satır
    while (gorevKol.Count > 0)
    {
        Task<int> gorev = await Task.WhenAny(gorevKol);
        int boyut = await gorev;
        listbox.Items.Add(boyut);
        gorevKol.Remove(gorev);
    }
}

Burada öncelikle IEnumerable nesnesi diziye değil, listeye dönüştürülmektedir. Bu liste nesnesi WhenAny() metoduna parametre olarak verilir. WhenAny() metodu ilk tamamlanan görevin görev nesnesini döndürür. Daha sonra await operatörüyle bu görevin ürettiği değer alınır ve listbox'a eklenir. Daha sonra bu görev listeden çıkarılır ve aynı işlem görev listesinde görev kalmayana kadar devam eder. Bu örnekte listbox bir anda değil, yavaş yavaş görevler tamamlandıkça dolar.