Asenkron (Asynchronous) Programlama Nedir?

Tarik Guney
Jun 9 · 7 min read

Uzun zamandan sonra başka bir yazımla karşınızdayım ve konumuz asenkron programlama. Devamlı asenkron yazmak zor olduğundan dolayı yazım boyunca buna async diyor olacağım. Şimdiye kadar hem yabancı hem de Türkçe kaynaklarda beni tatmin eden bir yazı çıkmadı karşıma ve ben de kendi yazımı yayınlamaya karar verdim. Bu konu hakkında okuduğum yazıların yazarlarına bakınca bende kalan izlenim: ya kendileri bile konuyu temelden anlamamışlar ve dolayısıyla okudukları yerleri tekrar ediyorlar ya da herkesin çoğu şeyi bildiğini farz ediyorlar. Yazının amacı async programlamanın uzaydan gelen bir şey olmadığını ve hayatımızın aslında ne kadar içinden çıktığını göstermek. Bunu yaparken de ufak tefek kodlar ile bunları örneklendireceğim.

Async programlama dendiğinde insanlardan ilk duyacağınız şey bloklamayan kod… Genelde açıklamalar burada kalır. Bloklamak nedir, bloklamayan nedir, vs. bunlar teorik olarak anlatılır ve dinleyeni tatmin etmez. Dinleyenlerin ise aklına ilk gelen şey ise birden fazla threadin kullanıldığı bir çözüm olur. Araç dediğinizde insanların aklına hemen dört tekerlekli bir arabanın gelmesi gibi. Motorsiklet var, kamyon var, uçak var… Async programlama dendiğinde aklınıza gelen şey yeni threadlerin oluşturulması değil, uzun bir işin bitmesini beklemeden bu işin sonucuna bağımlı olmayan diğer işlere devam edebilmek… bağımlı olan işleri ise beklenen işe bir devam şeklinde ekleyebilmek olmalı.

Örneklere geçmeden önce bir kaç bir mevzuyu açıklığa kavuşturmak gerekiyor. Bilgisayarda işlemler genelde iki türlü olur: CPU-bound ve IO-bound. Bunlar CPU yada IO cihazları üzerlerinde yapılan işlemlere verilen isimler. Mesela HTTP üzerinden bir siteye ulaşmaya çalıştığınızda işletim sistemi tarafından bu iş network kartınıza verilir (IO operasyonu). Bu noktada CPU bu konu hakkında çok bir şey yapmaz çünkü gerekli işlemler network kartının kendi devreleri ve chipleri üzerinde gerçekleşiyordur. CPU yapsa yapsa en fazla bir süre bekleyebilir. Ama işte biz beklemesini istemiyor ve var olan diğer işleri halletmesini istiyoruz. Bu noktada karşımıza async programlama çıkıyor.

Geçenlerde ailecek bir Hind yemeği yiyelim dedik ve ara sıra gittiğimiz bir restorana uğradık. Sağımızdan solumuzdan o kadar garson geçmesine rağmen hiç kimse siparişlerimizi almadı. 20 dakika saçma sapan bir şekilde bekledikten sonra böyle hizmet mi olur diyerek oradan ayrılıp bu sefer Pakistan lokantasına gittik. Bize böyle hizmet mi olur dedirten saçmalık siparişimizin bile alınmamasıydı. Eğer sipariş alınsa ve o şekilde 20 dk. bekleseydik belki de ayrılmazdık. Şimdi bu olaydan yola çıkalım ve async programlama basit bir restorana nasıl yardımcı olur anlamaya çalışalım.

Aklınıza dünyanın en saçma çalışan restoranını getirin dediğimde aklınıza gelecek olan şeylerden bir tanesi de sadece bir aşçı ve bir garsonun çalıştığı ve bir müşterinin siparişi kendisine servis edilmeden başka insanların siparişlerinin ALINMADIĞI bir yer olacaktır. Böyle bir restoran gerçekte var olsa, ata sporumuz olan linç ile orayı basar ve elimize geçirdiğimiz sandalye ve sopalar ile oradakileri döverdik herhalde. Peki böyle bir şeyin kodu neye benzerdi sizce?

Related image

Öncelikle en basitten bir servis sırasında yapılacak adımları kısaca listeleyelim:

  1. Müşteriye git ve kendisinden siparişini al.
  2. Mutfağa gidip aşçıya siparişin ne olduğunu söyle. Bu noktada aşçının siparişi hazır etmesini bekle. Senkron bir iş bu şekilde çalışır.
  3. Hazırlanan yemeği tezgahtan al.
  4. Yemeği müşteriye servis et.

Kod olarak yazalım:

static void Main(string[] args)
{
    Asci asci = new Asci();
    Garson garson = new Garson(asci);
    List<Musteri> musteriler = TumMusterileriListele();

Anlaşılması gayet kolay ve senkron olarak çalışan bir kod. Ama kolay anlaşılır olması kadar saçma ve yanlış bir kod. Eğer aşçının yemeği bitirmesi 30 dk. alacaksa, bizim garson bu süre boyunca hiç bir iş yapmadan boş boş bekleyecek demektir. Kaybedilecek olan parayı ve müşteriyi siz düşünün. Peki bu şekilde sorunlu bir çalışma mantığını nasıl düzeltebiliriz:

İlk adım olarak garsonun beklememesini sağlamak lazım. Beklememesi demek: Bir siparişi hazırlanması için aşçıya verdikten sonra, hazırlanması sırasında kendisi diğer müşteriler ile ilgilenebilir ya da masaları temizleyebilir, vs. Tamam, bu fena bir çözüm olmadı. İşte bu noktada artık async düşünmeye başladık demektir. Yemeğin hazırlanma süreci bizim garsonu artık bloklamıyor yani onu bekletmiyor. Aşağıdaki koda bir bakın.

static void Main(string[] args)
{
    Asci asci = new Asci();
    Garson garson = new Garson(asci);
    Musteri musteri = new Musteri();
    Siparis siparis = garson.SiparisAl(musteri);
    YemekDurumu yemekDurumu = asci.SiparisiHazirla(siparis);
    while (yemekDurumu.HalaHazırlanıyormu()){
        DakikaSayaci sayac = new DakikaSayaci();
        sayac.Basla();
        foreach(Masa masa in KirliMasalariGetir()){
            this.MasayiTemizle(masa);
            if(sayac.OnDakikaDoldu){
                break;
            }
        }
    }
    Yemek yemek = _asci.YemegiVer();
    musteri.YemegiGotur(yemek);
}

Evet…Kodumuz biraz daha karmaşık hale gelmeye başladı. Kısaca bir inceleyelim: Garson her 10 dakika da masaları temizleme işini bırakıp yemeğin hazır olup olmadığına bakıyor. yemekDurumu.HalaHazırlanıyormu()metodunun bir implementasyonu olarak ister yemeğin durumunu öğrenmek için tezgaha gider ve yemek gelmiş mi diye bakabilir, isterse gidip aşçıya bağırıp yemek hazır mı diye sorabilir, vs. Ama keşke bu garson devamlı mutfağa gidip gelmek zorunda kalmasa Bunun yerine ya aşçı yada orada çalışan başka birisi yemek hazır olduğunda bu garsonu çağırsa, daha iyi olmaz mıydı? Cevap için ileriki kısımlara devam.

Bu kod içinde artık async programlama daha rahat görülmeye başlıyor. Ama unutmayın, adımların lineer bir şekilde teker teker yazıldığı bir dünyada yani yazılım dünyasında async mantığını kullanmaya çalışıyoruz. Bu ise async olmasını istediğimiz kısımların kendisini çağıran diğer kodları bekletmemesi ile sağlanan bir kalite. _asci.SiparisiHazirla(siparis); kodu yemek nesnesi döndürmek yerine YemeginDurumu tipinden bir nesne döndürüyor. Bu nesne bir işin sonucunu değil, işin kendisini temsil ediyor. Dolayısıyla bu nesneye işin bitip bitmediğini sorabiliyoruz. Bu sayede garson bir sonuç beklemeden diğer işlere bakabiliyor. Ama async programlama sadece bir işi beklememek değil aynı zamanda iş bittiğinde o işin sonucuna bağlı olan başka işleri de devam şeklinde çalıştırabilmek demek. Bizim async çağrımızın devamı niteliğinde olan kod:

Yemek yemek = _asci.YemegiVer();
musteri.YemegiGotur(yemek);

Burada bir duralım bir thread meselesini biraz inceleyelim._asci.SiparisiHazirla(siparis); kodu hakkında burada thread kullanıyor mu sorusunu soranları duyar gibiyim. Kullanılmak zorunda değil, eğer bu bir I/O çağrısı ise… Uygulamanız sadece işletim sisteminin I/O cihazlarını kullanarak, mesela network kartı, hard disk, vs. yapmış olduğu işlemlerin bitmesini bekliyor. Burada sizin uygulamanızın yeni bir thread’e ihtiyacı yok. Hayır… aşçı ve garson aynı şey değil. Bizim threadlerimizi temsil eden varlıklar garsonlar. Aşçı bir network kartı veya bir hard disk. Sizin ise sadece bir garsonunuz ve dolayısıyla sadece bir tane threadiniz var.

Yukarıda ki kod güzel bir kod değil ve zaten de diğer müşterilerin siparişlerini yemeği beklerken almıyor. Almıyor çünkü garsonumuz aynı zamanda sadece bir siparişi aklında tutabilecek kafaya sahip. Ama neden aklında tutmak zorunda kalsın ki? Belki bir yönetici alsak ve bu yönetici aşçıdan gelen yemekleri bir yere biriktirse ve üzerlerine yemeklerin hangi masaya ait olduğuna dair bir kağıt yapıştırsa ve… bizim garson da alacak bir sipariş kalmadığında bu tezgahta sıra ile dizilmiş olan tabakları alıp üzerilerinde bulunan kağıtlarda yazan masalara götürüverse? Bu yönetici bir thread mi? Bu bizi ilgilendirmiyor, çünkü o bizim en samimi arkadaşımız olan runtime, yani çalışma zamanı mekanizmaları. Onu biz oluşturmadık ve biz kendisini yönetmiyoruz… Bilakis o bizi yönetiyor. İşte bu kağıtlar bizim için execution context dediğimiz bilgileri içeriyor. Tezgahtaki bu yemeklere message diyelim çünkü yemeğin bittiği ve müşteriye götürülmesi gerektiği mesajını veriyorlar bize. Yemeğin tezgaha konma sırasına bakınca ilk giren tabağın ilk olarak alınıp servis edildiği bir yapı görüyoruz ki buna queue yani kuyruk diyoruz. O zaman aklımıza ne geliyor: Message Queue. Bir tane thread yani garsonumuz var. Bir thread ve bir message queue… alın size JavaScript. Async çalışıyoruz, yeni threadlar oluşturmuyoruz. Peki, hani multithread olması lazımdı? Bize bakan tarafı ile yeni threadlar oluşturmuyoruz, ama runtime kendi işleri için threadlar oluşturabilir ki bu da bizi çok ilgilendirmiyor.

Peki Javascript gibi bir thread ve message queue ile async işlerin devamı olan kodları işleten bir sistemde ne gibi bir sorun olabilir? Diyelim ki müşteri çıkmış gitmiş. Bizim garson onu bulacağım diye sağa sola bakıyor, geziyor, bağırıyor, vs. ama bir türlü adamı bulamıyor. Saatler geçiyor ama hala adamın masasına gelmesini bekliyor, vs. Bu sefer de sırada bekleyen diğer tüm yemekler soğuyor… Diğer müşteriler moralleri bozuk bekliyorlar. Benzer başka bir sorun ise 100 tane müşterinin yemeklerini servis etmek için bir tane garson çok yavaş kalabilir. Bu gibi sorunları nasıl çözebiliriz?

Yukarıda ki sorun iki şekilde çözülebilir:

  1. Her bir servise zaman sınırlaması konabilir. Eğer alınan yemek 1 dakika içinde müşteriye servis edilemezse, o iş bırakılır ve tezgahta sırada bulunan diğer yemek alınır. Buna timeout diyoruz.
  2. Birden fazla garson işe alınır. Hangi garson müsaitse sıradaki yemeği tezgahtan alır ve müşteriye servis eder. Dolayısıyla müşteriden siparişi alan garson ile yemek hazırlandıktan sonra onu servis eden garson farklı olur. Siparişi mutfağa veren garson boşa çıkar ve diğer işleri yapması için kullanılır. Buna garson havuzu yada thread pool diyebiliriz.

İkinci çözümde birden fazla thread kullanabilmek C# gibi dilleri JavaScript gibi dillerden ayıran önemli özelliklerden bir tanesi.

Async programlama daha bir önem kazandıkça, dil tasarımcıları bunu nasıl daha kolay ve anlaşılır şekilde yazılıcımların kullanımına sunabiliriz sorusunu cevap aramaya başladılar. Mesela, bunlardan bir tanesi C# ve JavaScript gibi dillerin framework’lerinde sunulan Task ve Promise isminde tipler. Bunlar YemeginDurumu tipine benzer olarak bir işi temsil ediyorlar ama daha genel bir temsile sahipler. Güzel olan tarafları ise bittiklerinde çalışmasını istediğimiz kodları .ContinueWith() ya da .done() gibi fonksiyonları kullanmak suretiyle kendilerine rahatlıkla söyleyebiliyor olmamız:

static void Main(string[] args)
{
    Asci asci = new Asci();
    Garson garson = new Garson(asci);
    List<Musteri> musteriler = TumMusterileriListele();
    List<Task> bekleyenTumIsler = new List<Task>();
    foreach(Musteri musteri in musteriler)
    {
        Siparis siparis = garson.SiparisAl(musteri);
        Task<Yemek> yemekTask = asci.SiparisiHazirla(siparis);
        bekleyenTumIsler.Add(yemekTask)
        yemekTask.ContinueWith(t=>{
            Yemek yemek = _asci.YemegiVer();
            // Buradaki musteri nesnesini 
            // yemeklerin uzerine konan kagitta 
            // yazan bilgi gibi dusunebilirsiniz.
            musteri.YemegiGotur(yemek);
        });
    }
    Task.WaitAll(bekleyenTumIsler)
}

Yukarıda çokta süper olmayan ama async programlamanın basit ama önemli bir örneği var. Birden fazla müşterinin siparişini alıp, yemeklerinin hazırlanmasının beklemeden, diğer müşterinin siparişi almaya giden bir garsonun hikayesini anlatıyor. Yemekler hazır olduğunda ise ContinueWith() fonksiyonuna argüman olarak gönderilmiş olan fonksiyonu çalıştırıyor olacak.

En sonda ise dükkanı kapatmadan önce tüm işlerin hallediğinden emin olacak, bitmeyen siparişler varsa dükkanın kapanmamasını sağlayacak Task.WaitAll() çağrısı mevcut.

Tüm bunları da basitleştiren başka dil yapılarıda son yıllarda çoğu programlama dilinde boy göstermeye başladı ki bunlara async/await diyoruz ama o da başka bir yazıya kalsın.

Özet Olarak

Bitirmeden bir kaç birşeyi söylemek güzel olur. Async programming bir implementasyonu değil, bir davranışı tanımlar. Bu davranışı nasıl implement edeceğiniz değişir. Buradaki temel mantık, threadlerinizi uzun sürebilecek IO operasyonlarını beklemek ile bloklamamak ve beklenirken diğer işleri yapmak ve en son beklenen işin sonucu geldiği zaman onu bekleyen diğer kodların çalışması demektir. Bunu nasıl implement edeceğiniz ise kullandığınız teknolojiye kalmış. Beklenen sonucun devamında çalışacak kodu ise istersen bir thread ile ister birden fazla thread ile çalıştırın. Çokta sorun değil.

Umarım anlattıklarım anlaşılmıştır. Bir sonraki yazımda görüşmek üzere, kalın sağlıcakla…

Tarik Guney

Written by

Software Engineering Team Lead @ Motorola Solutions, PhD Candidate in Computer Science. Author of the book: Hands-on with Go: https://amzn.to/2QYFoaV