İlkay İlknur

just a developer...

C# 5.0 Paralel İşlemlerin Yönetimde Async & Await Kullanımı ve Task Combinator Metotları

Merhaba Arkadaşlar,

Önceki yazılarımızda sizlerle C# 5.0 ile gelecek olan Asenkron Programlama yeniliklerini gerek konseptsel olarak gerekse örnekler üzerinde uygulamalı olarak incelemiştik. Bu yazımızda ise C# 5.0 gelen Asenkron Programlama yeniliklerinin bir diğer kullanım alanını inceliyor olacağız.

.NET Framework 4.0’ın duyurulmasıyla beraber Framework içerisinde bildiğimiz gibi Parallel Programming konsepti kapsamında pek çok yenilik getirildi. Bu gelen yeniliklerle beraber artık uygulamalarımız içerisinde paralel olarak çeşitli süreçler çalıştırabilmekteyiz ve yönetimlerini de biraz zorlanarakta olsa sağlayabilmekteyiz. .NET Framework 4.0 içerisindeki Parallel Programlama yapısını inceldiğimizde bildiğimiz üzere herşeyin temelinde Task tipinin yattığını görmekteyiz. Aslında Task tipini incelediğimizde Paralel veya Asenkron bir işlemi temsil etmek için mükemmel bir tip olduğunu söyleyebiliriz. Bunun nedeni ise yapılan işlem ile ilgili tüm bilgileri içerisinde bulundurması. Asenkron işlemin durumunun kontrol edilmesi, içerisinde oluşan bir exception’ın elde edilmesi, asenkron işlemin tamamlanmasından hemen sonra çalıştırılacak olan işlemlerin belirtilmesi gibi tüm işlemleri bu tip üzerinden gerçekleştirebilmekteyiz. (Sanırım C# 5.0 ile gelen yeniliklerin nerede devreye gireceğini yavaş yavaş tahmin etmeye başladınız :) ) Evet bu yazımızın konusu Paralel işlemlerin yönetiminde Async ve Await keywordlerinin kullanımı olacak.

C# 5.0 ile beraber gelen yenilikleri uygulamalı olarak incelediğimiz yazımızdaki kısımları hatırlamamız gerekirse await keywordü sayesinde Task,Task<T> veya void dönüş tipine sahip olan asenkron işlemlerinin yönetimini sağlayabiliyorduk. Bu nedenle paralel işlemlerini yönetimini de aynı şekilde sağlayabileceğimizi düşünebiliriz. Örneğin,  Task.Factory.StartNew  metodunu kullanarak bir işlemi arka planda işletmeye başladığımızda StartNew metodu bize Task tipinde bir nesne örneği döndürmekte. İşte tam da bu noktada Async ve Await keywordleri devreye girmekte ve bu şekilde paralel işlemlerin yönetimini kolay bir şekilde sağlayabilmekteyiz.

Şimdi hemen Visual Studio üzerinden bir WPF uygulaması yaratalım ve ilgili AsyncCTPLibrary dll’ini projemize referans olarak ekleyelim ve uygulamamızı geliştirmeye başlayalım.

Uygulamamızın arayüzü oldukça basit olacak ve sadece 1 adet Textblock ve Button’dan oluşuyor olacak.

<Window x:Class="AsyncAwaitParallel.MainWindow" 
xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
Title="MainWindow" Height="350" Width="525">     
<Grid>         
<Button Content="Compute" Height="23" HorizontalAlignment="Left" 
 Margin="12,12,0,0" Name="button1" VerticalAlignment="Top" 
 Width="75" Click="button1_Click" />         
<TextBlock HorizontalAlignment="Left" Margin="12,41,0,0” 
Name="txtResult"VerticalAlignment="Top" />     
</Grid> 
</Window>

Uygulama içerisinde ise aslında bizim için hiçbir anlamı olmayacak ancak CPU’yu oldukça yoran ve maksimum düzeyde çalıştıran işlemler gerçekleştireceğiz. Bu işlemler de Math tipi içerisinde bulunan Sqrt, Sin ve Tan fonksiyonları olacak. Basit bir for döngüsü içerisinde bu fonksiyonları paralel olarak arka planda çalıştırıp elde ettiğimiz değerleri topluyor olacağız. Daha sonra ise topladığımız değerleri de ekrandaki textbox’a yazdırıyor olacağız.

private void button1_Click(object sender, RoutedEventArgs e)
{
 double SqrtSum = 0, SinSum = 0, TanSum = 0;
 Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {                         
 SqrtSum += Math.Sqrt(i);
 }                 
 }).ContinueWith((t) => {
 txtResult.Text += String.Format("SqrtSum = {0}", SqrtSum)});

 Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 SinSum += Math.Sin(i);
 }
 }).ContinueWith((t) => {
 txtResult.Text += String.Format("SinSum = {0}", SinSum)});             

 Task.Factory.StartNew(() =>
 {                 
 for (int i = 0; i < 50000000; i++)
 {
 TanSum += Math.Tan(i);
 }
 }).ContinueWith((t) => {
 txtResult.Text += String.Format("SqrtSum = {0}", SqrtSum)});
}

Evet gördüğünüz gibi herbir CPU yoğun işlemi Task.Factory.StartNew metodunu kullanarak arka plana aldık. Böylece hem arayüzümüzün cevapsız kalmasının önüne geçerek uygulamamızın donmamasını sağladık. Hem de arka planda CPU’nun etkin bir biçimde tüm çekirdeklerini kullanmasını sağladık. Şimdi isterseniz uygulamamızı çalıştıralım.

Upps !!! İşte karşımızda asenkron işlem çalıştırma bunalımı :) Arka planda bir işlem gerçekleştiriyorsunuz ve bunun sonucu ekrana yazamıyorsunuz çünkü ekranda kullanacağımız kontrol başka bir thread’e ait :). Neyse şimdilik bu kısmı pas geçelim ve MessageBox.Show kullanarak sonuçları ekran görüntüleyelim. Ancak yazımızın ilerleyen bölümlerinde bu noktaya değiniyor olacağız. ;) MessageBox.Show metodunu kullarak elde ettiğimiz görüntüler ise şu şekilde.

Uygulamayı çalıştırdığımızda işlemler çeşitli sıralarda arka planda çalıştırıldı ve Task sona erdiğinde sonucu MessageBox kullanarak görüntüledik. Ancak baktığımızda yine önceki yazılarımızda da değindiğimiz Callback tabanlı bir yaklaşım izledik. ContinueWith metodunu kullanarak içerisinde lambda ifadesi yardımıyla Task’ın sonlamasından sonra çalışacak işlemleri belirttik. Peki ya task bittikten sonra başka bir taskın çalışmasını isteseydik ve o task’ta bittiğinde başka bir taskı başlatsaydı :)

Task.Factory.StartNew(() => { }).ContinueWith((t) =>
{
Task.Factory.StartNew(() => { }).ContinueWith(k =>
 {                         
 });                 
});

Yukarıda gördüğümüz gibi yine iç içe girmiş ifadelerle karşılaşıyor olacağız. Aslında yine başa döndük diyebiliriz. Asenkron programlamada da aynı bunalıma düşmüştük ve C# 5.0 ile bu bunalımdan hızlıca çıkmıştık. Öyleyse hemen C# 5.0 ile uygulamamızı yeniden düzenleyelim. Öncelikle metodumuz içerisinde Task tipi üzerinden paralel işlemlerin yönetimini yapacağımızdan dolayı metodumuzun başına async modifier’ını ekliyoruz.

Sonrasında ise ilk olarak her Task.Factory.StartNew metodunu kullandığımız ifadelerin başına await keyword’ünü yerleştirelim (StartNew metodu Task tipini döndürdüğünden dolayı await ifadesini kullanabilmekteyiz.) ve sonrasında o işlem sonucunda elde ettiğimiz değerleri ekrana yazdıralım.

private async void button1_Click(object sender, RoutedEventArgs e)
{
 double SqrtSum = 0, SinSum = 0, TanSum = 0;
 await Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 SqrtSum += Math.Sqrt(i);
 }
 });
 txtResult.Text += String.Format("SqrtSum = {0}", SqrtSum);

 await Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 SinSum += Math.Sin(i);
 }
 });
 txtResult.Text += String.Format("SinSum = {0}", SinSum);
 await Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 TanSum += Math.Tan(i);
 }
 });

 txtResult.Text += String.Format("\nTanSum = {0}", TanSum);
}

Uygulamayı çalıştırdığımızda sonuçların ekranda göründüğünü görüyor olacağız.

Ancak uygulamayı çalıştırdığımızda sonuçların ekranlara yazılma sırasının hep sqrt,sin ve tan şeklinde olduğunu farketmişinizdir. Acaba gerçekleştirdiğimiz kullanım kodumuzunun paralel olarak işletilmesinin önüne mi geçiyor ? Şimdi gelin isterseniz await ifadesinin kullanımını bir hatırlayalım.

Compiler, await keyword’ünü gördüğü noktadan itibaren mevcut ifadenin sonrasında işletilecek olan ifadeleri bir devam kodu olarak işaretlemekte. Yani mevcut işlem sonlandırıldıktan sonra diğer ifadeleri işletmekte. Böyle olunca da aslında önce Sqrt işlemi gerçekleşmekte sonrasında da Sin ve Sin işleminden sonra da Tan hesaplaması yapılmakta. Peki kodumuzu nasıl paralel olarak işleteceğiz. Aslında sadece 1-2 ufak değişiklik yapmamız bizim için yeterli.

private async void button1_Click(object sender, RoutedEventArgs e)
{
 double SqrtSum = 0, SinSum = 0, TanSum = 0;
 Task t1 = Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 SqrtSum += Math.Sqrt(i);
 }
 });
 Task t2 = Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 SinSum += Math.Sin(i);
 }
 });
 Task t3 = Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 TanSum += Math.Tan(i);
 }
 });
 await t1;
 await t2;
 await t3;
 txtResult.Text += String.Format("SqrtSum = {0}", SqrtSum);
 txtResult.Text += String.Format("SinSum = {0}", SinSum);
 txtResult.Text += String.Format("\nTanSum = {0}", TanSum);
}

Yukarıdaki gibi kodumuzu değiştirirsek aslında istediğimiz paralel işletimi sağlamış olmaktayız. Çünkü öncelikle işlemleri arka planda başlatıyoruz ve sonrasında await ifadelerini kullanıyoruz , böylece de işlemlerin sona ermesinden sonra oluşan değerleri ekrana yazdırıyoruz. Ayrıca ekrana yazdırma işlemini artık başka bir thread içerisinde yapmadığımızdan dolayı doğrudan arayüz threadi üzerinden işlemleri gerçekleştirmemiz sayesinde yukarıda düşmüş olduğumuz asenkron işlem bunalımına burada düşmemiş oluyoruz ;)

Evet arkadaşlar gördüğünüz gibi paralel işlemlerin yönetimini de async ve await keywordleri ile sağlayabilmekteyiz. Eğer taskları aynı anda başlatmak isterseniz yukarıdaki gibi bir kullanım gerçekleştirebilirsiniz. Bunun yanında eğer tasklarınız birbirine bağımlıysa ve biri bittikten sonra diğerinin başlaması gerekiyorsa da bir önceki kullanım tam size göre olacaktır.

Taskları teker teker yönetmenin yanında toplu olarak yönetmemizi sağlayan Task Combinators adını verdiğimiz metotlar hali hazırda .NET Framework içerisinde bulunmakta. Bu metotlar uygulamamız içerisinde yarattığımız tüm taskların işletilmesinin sona ermesini beklemek gibi işlemler gerçekleştirebilmekte. Ancak bu metotlar bool değer döndürdüğü için örnek olarak tüm taskların işletiminin sona erdikten sonra bir takım işlemleri gerçekleştirmemiz oldukça sıkıntılı olmakta. Bu nedenle .NET Framework 4.5 ile Task tipi içerisine dönüş tipi Task olan Combinator metotlar ekleniyor olacak. Şimdi gelin Task Combinator metotlarını kısaca inceleyelim.

Task Combinators

Not : Task Combinators olarak bahsettiğimiz metotlar eğer Visual Studio 2010 kullanıyorsanız projenize referans olarak eklemiş olduğumuz AsyncCTPLibrary  dll’i içerisindeki TaskEx tipi içerisinde bulunmakta. Bunun nedeni makinanızda .NET Framework 4.0 kurulu olduğundan dolayı mevcut metotları bozmamak. Ancak Visual Studio 2011 kullanıyorsanız .NET Framework 4.5 yüklü olacağından dolayı ilgili metotlar Task tipi içerisinde olacaktır ve TaskEx tipi burada bulunmayacaktır. Biz örneklerimizi Visual Studio 2010 üzerinden geliştirdiğimiz için TaskEx tipi üzerinden ilerliyor olacağız.

TaskEx tipi içerisinde bulunan static metotları incelediğimizde parametre olarak IEnumerable<Task> tipinden parametreler alan WhenAll, WhenAny isimli Task tipinden bir nesne örneği döndüren metotları görmekteyiz. Bu metotlar önceden de bildiğimiz gibi yaratmış olduğumuz taskların sonlanmasını beklemekte. Ancak Task tipinden bir nesne örneği döndürdüğünden dolayı async ve await keywordlerini kullanarak tüm taskların bitiminden sonra istediğimiz işlemleri gerçekleştirebilmekteyiz.

private async void button1_Click(object sender, RoutedEventArgs e)
{
 double SqrtSum = 0, SinSum = 0, TanSum = 0;
 Task t1 = Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 SqrtSum += Math.Sqrt(i);
 }
 });
 Task t2 = Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 SinSum += Math.Sin(i);
 }
 });
 Task t3 = Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 TanSum += Math.Tan(i);
 }
 });

 await TaskEx.WhenAll(t1, t2, t3);
 txtResult.Text += String.Format("SqrtSum = {0}", SqrtSum);
 txtResult.Text += String.Format("SinSum = {0}", SinSum);
 txtResult.Text += String.Format("\nTanSum = {0}", TanSum);
}

Yukarıdaki gerçekleştirime baktığımızda WhenAll metoduna yaratmış olduğumuz taskları parametre olarak geçirerek tüm tasklar içerisindeki hesaplama işlemlerinin bitmesini bekledik ve await keyword’ü sayesinde hesaplama işlemi sona erdikten sonrada ilgili sonuçları ekrana yazdırmış olduk.

TaskEx tipine baktığımızda pek çok farklı Combinator metot bulunmakta. Bu combinator metotların yanında bir de Delay isimli bir metot olduğunu görmekteyiz. Bu metot ta aslında Task tipinden bir nesne örneği döndüren Thread.Sleep metodunun ta kendisi :) Ancak tabi ki Task tipi işin içerisine girdiğinden dolayı await keywordü ile çok kolay bir şekilde bekletme işi sona erdikten sonra istediğimiz işlemleri gerçekleştirebiliriz. Örneğin tüm tasklar sona erdikten her bir sonucu 1’er saniye ara ile ekrandaki kontrolümüze yazabiliriz.

private async void button1_Click(object sender, RoutedEventArgs e)
{
 double SqrtSum = 0, SinSum = 0, TanSum = 0;
 Task t1 = Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 SqrtSum += Math.Sqrt(i);
 }
 });
 Task t2 = Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 SinSum += Math.Sin(i);
 }
 });
 Task t3 = Task.Factory.StartNew(() =>
 {
 for (int i = 0; i < 50000000; i++)
 {
 TanSum += Math.Tan(i);
 }
 });

 await TaskEx.WhenAll(t1, t2, t3);
 txtResult.Text += String.Format("SqrtSum = {0}", SqrtSum);
 await TaskEx.Delay(1000);
 txtResult.Text += String.Format("SinSum = {0}", SinSum);
 await TaskEx.Delay(1000);
 txtResult.Text += String.Format("\nTanSum = {0}", TanSum);
}

Gördüğümüz gibi Delay metodu 1 saniye sürecek olan bir Task yarattı ve biz de await ile bu task’ın sona ermesinden sonra çalışacak ifade olarak ekrana sonucu yazdıracak olan ifadeyi bildirdik.

Evet arkadaşlar gördüğünüz gibi aslında async ve await keywordlerini pek çok farklı noktada kullanabilmekteyiz. Ancak baktığımızda aslında Task tipini temel olarak alan işlemlerde await keywordünü kullanabildiğimizi gördünüz. Bu nedenle de Framework içerisinde bulunan metotlarda da Task tabanlı bir altyapıya geçiş yapılmakta. Bu şekilde programlama dili tarafından sunulan kolaylıklar framework ile de desteklenmekte. Await kullanımı için aslında bir takım belirli prensipler bulunmakta. Await kullanacağımız ifadenin tipinin Task,Task<T> veya void olması gibi. Bu şekilde kullanımlara aslında Awaitable Pattern adını veriyoruz. Yani gerçekleştirdiğiniz altyapı awaitable pattern’a uygun ise kolaylıkla async & await ifadesini kullanabilirsiniz. ;)

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

Bir sonraki yazımızda görüşmek üzere,

Hoşçakalın

Not : Uygulama Visual Studio Async CTP3 ile geliştirilmiştir.



Yorum Gönder


Yorumlar

  • profile

    Ikram Turgunbaev

    • 4
    • 7
    • 2017

    Asenkron metotlarla ilgili okuduğum en açıklayıcı, anlaşılır makale! Teşekkürler!

  • profile

    osman tağmur

    • 10
    • 9
    • 2015

    10 numara 5 yıldız böyle adamları yazmaya teşvik etmeli çok teşekkürler

  • profile

    İlkay İlknur

    • 16
    • 4
    • 2014

    Download işlemi bittikten sonra zaten UI threade dönmüyor mu ? Dönüyorsa zaten bir sorun olmaz. Ama hala daha başka bir thread'de işler yapıyorsan download task'ına continuation yazarak ilerleyebilirsin. task.ContinueWith(() => { dispatcher.Invoke(new Action(() => { this.TextBlock1.Text = "Complete"; } });

  • profile

    Ahmet

    • 14
    • 4
    • 2014

    Merhaba benim şöyle bir sıkıntım var.sizin örnekten gidelim. ben bütün taskların bitmesini beklemeden task bitti anda gelen sonucu textbox'a yazdırıyor olsaydım ve 2 Task' aynı anda bitmiş olsaydı o zaman aynı anda 2 Thread bir textBox'a erişiyor olacaktı.Bu durumu nasıl aşarız.? Bendeki örnek 8 farklı siteden aynı anda veri çeken ve aynı anda gelen verileri gride yazan bir uygulamam var bu şekilde bir sıkıntı çekiyorum ne önerirsiniz.

  • profile

    Abdurrahman Güngör

    • 28
    • 3
    • 2014

    Çok güzel bir makale olmuş. Gerçekten anlaşılır ve detaylı. Eline sağlık.

  • profile

    Bülent ERDEM

    • 3
    • 4
    • 2013

    Ellerinize sağık çok anlaşılır bir makale olmuş.