Burak Selim Senyurt(MVP)
Matematik Mühendisi bir .NET Severin Yazıları...

FORParallelism

Çarşamba, 16 Aralık 2009 13:55 by bsenyurt

Merhaba Arkadaşlar,

Günümüz yazılım teknolojilerinin belkide en popüler olan konularından biriside paralel programlamadır(Parallel Programming). Özellikle kullanıcı bilgisayarlarının artık birden fazla çekirdeğe sahip işlemcilerle donatılmış olduğu düşünüldüğünde geliştirme ortamlarının da(.Net Framework 4.0' da olduğu üzere Wink ) paralel programlamaya daha fazla destek vermeye başladığını görmekteyiz. Aslında zaten var olan araçlar ile paralel programlama tekniklerini uygulayabilmekteyiz. Ne varki kodlanmasının karmaşık olması bir yana, birden fazla tekniğin kullanılabiliyor olması, hangisinin daha performanslı olduğunun anlaşılması için test aşamalarının da önemini ortaya çıkarmakta. Microsoft cephesi bir süredir, paralel programlama kütüphanesi ile söz konusu tekniklere ait tasarımları aza indirgeyip kolay geliştirilebilir ve performanslı sonuçlar üreten tiplerin tasarlanması ve geliştirilmesini gereçekleştirmekte. .Net Framework 4.0 içerisinde doğrudan gelen Task Parallel Library kütüphanesi bu anlamda önemli kabiliyetler içermekte.

Peki elimizde bu kütüphane olmasaydı? Sealed O zaman n sayıda tekrar edecek olan bir işlemi paralel hale getirmek için nasıl bir kodlama yapmamız gerekirdi?

Söz gelimi başlangıç ve bitiş değerleri parametrik olan bir döngünün içerisinden çağırılan bir fonksiyonun, birden fazla Thread' e bölünerek çalıştırılmasını istediğimizi düşünelim. Aslında teorik olarak makinede kaç işlemci yada kaç çekirdek var ise o sayıda Thread açılması tercih edilir. Buna göre tekrar edecek olan işlemler belirli aralıklara bölünerek bu aralıkların açılan Thread' ler tarafından ele alınması sağlanır. Ne demek istediğimi aşağıdaki örnek kod parçası ile aktarmaya çalışayım.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Parallelism
{
    class Program
    {
        static void Main(string[] args)
        {
            ParallelFor(48, 98,
                (i) =>
                {
                    double r = 0;
                    for (int j = 0; j < i * 99999; j++)
                        r = Math.Sqrt((i * Math.PI) / Math.E);
                    Console.Write("{0} ", i.ToString());
                }
            );

            Console.ReadLine();
        }

        static void ParallelFor(int lowerBound, int upperBound, Action<int> body)
        {
            int processorCount = Environment.ProcessorCount; // İşlemci/çekirdek sayısı bulundu
            int range = (upperBound - lowerBound) / processorCount; // Yaklaşık iterasyon sayısı hesaplanır.
            Console.WriteLine("İşlemci/Çekirdek Sayısı : {0} , Iterasyon Boyutu {1}\n", processorCount.ToString(), range.ToString());

            #region Birinci Yöntem (List<Thread> Kullanımı)

            List<Thread> threads = new List<Thread>(processorCount); // İşlemci/çekirdek sayısı kadar Thread taşıyacak koleksiyon tanımlanır.

            // İşlemci/çekirdek sayısı kadar çalışacak bir döngü
            for (int processor = 0; processor < processorCount; processor++)
            {
                // Thread tarafından ele alınacak değer aralığı hesaplanır
                int startPoint = (processor * range) + lowerBound;
                int endPoint = (processor == processor - 1) ? upperBound : startPoint + range;
                Console.WriteLine("Start : {0} End : {1}", startPoint.ToString(), endPoint.ToString());
                // Her bir çekirdek için bir Thread oluşturulur ve içerisinde iterasyon aralığı uzunluğunda bir döngü oluşturularak parametre olarak gelen fonksiyon çalıştırılır
                threads.Add(new Thread(() =>
                    {
                        for (int i = startPoint; i < endPoint; i++)
                        {
                            body(i);
                        }
                    }
                )
                );
            }

            // Thread' ler çalıştırılır
            foreach (Thread t in threads)
            {
                t.Start();
            }
            // Thread' lerin tamamlanması beklenir
            foreach (Thread t in threads)
            {
                t.Join();
            }
            #endregion
        }
    }
}

Örneğimizde tamamen anlamsız olan bir döngü çalıştırıldığını görmektesiniz. İçerdiği bazı hesaplamalar sayesinde zaman alan bir işlemler bütünü söz konusu. Burada söz konusu olan operasyonun birden fazla Thread' e bölünerek çalıştırılması içinde ParallelFor isimli yardımcı metoddan yararlanılmaktadır. Bu modelde ParallelFor isimli metod döngünün başlangıç ve bitiş değerlerini almakta, ayrıca çalıştıracağı fonksiyonu işaret eden Action<T> tipinden bir parametre almaktadır. Metoda göre Thread' lerin değerlendireceği aralıklar hesaplanır. Thread' ler basit bir List<T> koleksiyonunda tutulmakta olup çalıştırılmaları ve tamamlanmalarının beklenmeleri için iki foreach döngüsünden yararlanılır. İlk döngü oluşturulan Thread' leri başlatırken diğeride oluşturulan Thread' leri Main Thread' e katıp uygulamanın sonlanması için söz konusu Thread' lerin işlerinin bitirilmesinin beklenmesini garanti etmektedir. Dikkat edilmesi gereken nokta işlemci/çekirdek sayısı kadar Thread oluşturulması ve oluşturulan her bir Thread' in yaklaşık olarak hesap edilen iterasyon alanı kadar değeri hesaba katarak Action<T> ile gelen operasyonu çalıştırmasıdır.

Peki çalışma zamanındaki durum nedir?

Bu konuda çok şanslıyız nitekim Visual Studio 2010 ile birlikte son derece etkili performans analiz araçları gelmekte. Geliştirmeyi yapmakta olduğumuz Visual Studio 2010 Ultimate Beta 2 sürümünde yer alan Concurrency Profiler raporuna bakıldığında, yukarıdaki örnek için aşağıdaki sonuçların elde edildiği görülür.

İlk çalışmanın sonucu oluşan ekran görüntüsü;

İlk çalışma sonucu elde edilen Concurrency Profiler çıktısı;

Sarı alanlar bir Thread' in çalışmakta olduğunu ama diğer bir Thread tarafından o süre boyunca etkisizleştirildiğini göstermektedir. Yeşil renkli alanlar Thread' in işini yaptığı zaman aralıklarıdır. Sarı bölgelerin fazla olması performansı olumsuz yönde etkileyen bir faktördür. Nitekim aşırı talebin(Oversubscription) oluştuğunu göstermektedir. Peki iyileştirmenin bir yolu olabilir mi? Aslında ThreadPool tipinden yararlanarak Therad yönetiminin sisteme bırakılması sağlanabilir. İşte buna göre yazılan yeni modelimiz;

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Parallelism
{
    class Program
    {
        static void Main(string[] args)
        {
            ParallelFor(48, 98,
                (i) =>
                {
                    double r = 0;
                    for (int j = 0; j < i * 99999; j++)
                        r = Math.Sqrt((i * Math.PI) / Math.E);
                    Console.Write("{0} ", i.ToString());
                }
            );

            Console.ReadLine();
        }

        static void ParallelFor(int lowerBound, int upperBound, Action<int> body)
        {
            int processorCount = Environment.ProcessorCount; // İşlemci/çekirdek sayısı bulundu
            int range = (upperBound - lowerBound) / processorCount; // Yaklaşık iterasyon sayısı hesaplanır.
            Console.WriteLine("İşlemci/Çekirdek Sayısı : {0} , Iterasyon Boyutu {1}\n", processorCount.ToString(), range.ToString());

            #region İkinci Yöntem (ThreadPool)

            int remainingProcessor = processorCount;
            // ManualResetEvent bir olay meydana geldiğinde beklemekte olan bir veya daha çok Thread' e bilgilendirmede bulunur.
            ManualResetEvent manuelResetEvent = new ManualResetEvent(false);
            for (int processor = 0; processor < processorCount; processor++)
            {
                int startPoint = (processor * range) + lowerBound;
                int endPoint = (processor == processorCount - 1) ? upperBound : startPoint + range;

                // ThreadPool, Thread' ler için bir havuz sağlar ve asenkron işleyişin yönetimini sağlar
                // QueueUserWorkItem yürütülmek üzere bir metodu kuyruğa atar.ThreadPool içerisindeki ilgili thread kullanılabilir olduğunda da metod icra edilir.
                ThreadPool.QueueUserWorkItem((o) =>
                    {
                        for (int i = startPoint; i < endPoint; i++)
                        {
                            body(i);
                        }
                        // Birden fazla Thread tarafından kullanılan remainingProcessor değeri azaltılır ve 0 olup olmadığı kontrol edilir. Eğer 0 ise bekleyen tüm Thread' ler için sinyal verilir
                        if (Interlocked.Decrement(ref remainingProcessor) == 0)
                            manuelResetEvent.Set();
                    }
                );

            }
            // Diğer Thread' ler tamamlanıncaya kadar(ki Set dolayısıyla sinyal geldiğinde anlaşılır) ana thread' i bekletecektir.
            manuelResetEvent.WaitOne();
            manuelResetEvent.Close(); // Güncel WaitHandle ile alakalı tüm kaynaklar serbest bırakılır(Release).

            #endregion
        }
    }
}

Bu sefer ThreadPool tipinin QueueUserWorkItem static metodu kullanılmıştır. ThreadPool bir önceki modele göre Thread yönetimini daha iyi yapmaktadır. Öyleyse yeni modele göre oluşan çalışma zamanı Thread analizine bir bakalım.

İkinci modele göre çalışma sonucu;

İkinci modele göre Concurrency Profiler çıktısı;

Bu rapora göre Thread' lerin toplam çalışma sürelerinin bir önceki modele göre azaldığı görülmektedir. Sarı alanların süresi daha az görünsede sayıları yinede çok azalmış değildir. Dolayısıyla aşırı talep(Oversubscription) durumu devam ediyor görünmektedir. Ancak ana Thread' in sadece Thread' lerin çalışması tamamlanıncaya kadar bloklandığı gözlemlenmektedir. Peki yeni bir yöntem tercih edilebilir mi? Evet edilebilir. Aslında ThreadPool kullanımının iyileştirilmesi yoluna gidilebilir ki biz daha fazla ilerlemeyeceğiz...

Gördüğünüz üzere çoğu geliştirici açısından ileri seviyede kalan bir kodlama gerekmektedir. Özellikle geliştiricinin Thread konusuna son derece iyi hakim olması şarttır. Her ne kadar söz konusu karmaşık teknikler birer tasarım kalıbı olarak şekillenmiş olsalarda geliştiricinin kafa ayarını da fazla çizdirmemek gerekir. Buda yazımızın neden kafayı çizmiş bir bilgisayarcı resmi ile başladığının ispatıdırLaughing İşte Task Parallel Library ile birlikte gelen tipler bu anlamda işleri kolaylaştırmaktadır. Ama tabiki Concurrency Profiler ile üretilen rapor sonuçlarını değerlendirmek gerekir.(Bu tip karmaşık teknikleri tercih ederken kişisel görüşüme göre programcının performans mı? kolay ve hızlı kodlama mı? sorusuna verdiği cevap büyük önem kazanmaktadır) İşte aynı süreç için Parallel.For kullanımı ve rapor sonuçları;

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Parallelism
{
    class Program
    {
        static void Main(string[] args)
        {
            Parallel.For(48, 98, (i) =>
                {
                    double r = 0;
                    for (int j = 0; j < i * 99999; j++)
                        r = Math.Sqrt((i * Math.PI) / Math.E);
                    Console.Write("{0} ", i.ToString());
                }
            );

            Console.ReadLine();
        }
    }
}

Parallel.For kullanımının sonucu;

Parallel.For kullanımına göre Concurrency Profiler çıktısı;

Bu rapora göre Thread işlemlerinin tamamlanma süresinin çok daha azaldığı görülmektedir. Ayrıca Sarı alanların sayısında belirgin ölçüde azalma gözlemlenmektedir. İlginç olan noktalardan biriside Main Thread' de bloklanmanın(Kırmızı alanlar) diğer modellere göre çok daha az sayıda olmasıdır. Bir başka deyişle aşırı talep(Oversubscription) durumu biraz daha azalmıştır.

Raporlar ile ilişkili not: Döngülerin çalışma zamanında açtıkları Thread' lerin çalışma şeklini raporlamak amacıyla Visual Studio 2010 ile birlikte gelen Debug menüsündeki Start Peformance Analysis öğesi kullanılmaktadır. Bu öğe ile açılan sihirbazda bizim örneğimiz için Concurrency seçeneği işaretlenmelidir. Ayrıca bu seçeneğin alt seçimi olan Visualize the behavior of multithreaded application kutucuğunun işaretlenmiş olması da gerekmektedir. Ancak bu son seçenek Windows 7, Server 2008 işletim sistemleri üzerinde kullanılabilmektedir. Raporların oluşturulması programın çalıştırılması ile birlikte başlamaktadır. Bu nedenle söz konusu analiz raporlarının üretilmesi zaman alabilir. Raporlarda n sayıda Thread' in görülmesi mümkündür. Örneklerimizdeki analizlerimizi kolayca incelemek için sadece ilgili Thread' lerin çalışma zamanı durumları göz önüne alınmıştır. Diğerleri ise gizlenmiştir. Üretilen analiz raporundaki Threads kısmında yer alan renklerin belirli anlamları vardır. Sarı renkler Preemption olarak adlandırılmakta olup genellikle aşırı talep(Oversubscription) ile ilişkili süreleri belirtmektedir. Yeşil alanlar Thread' in iş yaptığını gösteren zaman aralıkları iken kırmızı alanlar bloklama yapılan zaman aralıklarını ifade etmektedir.

Tabiki bu test sonuçları, uygulamanın çalıştığı sistemin donanımsal özelliklerine göre değişiklik gösterecektir. Ancak sonuç olarak Parallel.For döngüsünün paralel işlemleri daha efektif olarak yürüttüğünü düşünebiliriz. Bunlara ek olarak aslında Parallel.For döngüsünün sağladığı başka avantajlarda vardır. Bunlar aşağıda listelenmiştir.

  • Etkili Yük Dengelemesi (Load Balancing) : Parallel.For Thread' ler arasındaki yük dağılımını organize eder.
  • Dinamik Thread Sayısı : Parallel.For akılldır ve zaman aşımları durumunda döngü içerisindeki Thread sayılarını dinamik olarak ayarlayabilir.
  • Yüksek Değer Aralıkları : Parallel.For metodu Int32 dışında Int64 tipini de kullanılabilir.
  • Konfigurasyon Seçenekleri : Örneğin Parallel.For içerisinde açılacak olan Thread sayısı için limit belirlenebilir.
  • İstisna Yönetimi (Exception Handling) : Döngü içerisinde bir istisna oluştuğunda, dahil olan tüm Thread' ler mümkün olduğunca kısa sürede işlemlerini durdururlar. Aslında varsayılan olarak yeni iterasyonların başlaması durdurulur. Bu nedenle exception sonrası Thread' lerin yürüttüğü bazı iterasyon adımları devam edebilir ancak yenilerinin başlatılması exception nedeniyle engellenir.
  • İç içe paralellik (Nested Parallelism) :İç içe çalıştırılan Parallel.For döngüleri birbirlerinin Thread kaynaklarını koordineli olarak paylaşarak çalışırlar.
  • vb...

Şimdi bu avantajları kendi yazdığımız ParallelFor metodu içinde gerçellemeye çalıştığımızı düşünelim. Hatta deneyin Wink Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

TaskParallelLibrary.rar (89,38 kb)

Tags:  
Categories:   TPL
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

Task Parallel Library(TPL) - Detached Tasks [Beta 2]

Perşembe, 12 Kasım 2009 15:00 by bsenyurt

Merhaba Arkadaşlar,

Bir önceki yazımızda Task Parallel Library tarafında .Net Framework 4.0 Beta 2 tabanlı olarak iptal işlemleri(Task Cancellation) için yapılan değişikliklere değinmeye çalışmıştık. TPL tarafında yapılan değişikliklerden birisi de iç içe çalışan Task' ler arasındaki Parent - Child ilişkiye yönelik olarak yapılmıştır. Aslında basit bir davranış değişikliği olduğunu söyleyebiliriz. Konuyu daha net kavramak amacıyla aşağıdaki örnek kod parçasını göz önüne alalım.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace DetachedTasks
{
    class Program
    {
        static void Main(string[] args)
        {
            Task task1 = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Task 1 başlangıç zamanı {0}",DateTime.Now.ToLongTimeString());
                Task task2 = Task.Factory.StartNew(() =>
                    {
                        Console.WriteLine("Task 2 başlangıç zamanı {0}", DateTime.Now.ToLongTimeString());
                        Thread.Sleep(6000);
                    }
                );
                Thread.Sleep(3000);
            }
            );

            task1.Wait();
            Console.WriteLine("Program sonu :  {0}",DateTime.Now.ToLongTimeString());
        }
    }
}

Öncelikli olarak kodumuzda neler yaptığımıza bir bakalım. Örnekte iki ayrı Task nesnesinin kullanıldığı görülmektedir. task2 isimli nesne örneği, task1 içerisinde üretilmekte ve kullanılmaktadır. Buna göre task1' in, task2' nin parent' ı olması gerektiğini düşünebiliriz. Aslında önemli olan nokta task1 üzerinden yapılan Wait çağrısı sonucu programın nasıl davranacağıdır. Bu noktada task1.Wait() çağrısının Beta 1 sürümünde farklı değerlendirildiğini belirtmemiz gerekiyor. Şöyle ki;

Beta 1 sürümüne göre task2 otomatik olarak task1' e bağımlı hale getirilmekteydi(Attached Task). Yani task1 üzerinden yapılan Wait çağrısı sonucu task1' in tamamlanması beklenirken, içeride çalışmakta olan task2' ninde çalışmasını tamamlanması gerekmekteydi. Bu varsayılan davranış biçimiydi. Ancak bazı durumlarda task1 beklenirken, içerisinde yer alan task2' nin beklemesi istenmeyebilir. Bir başka deyişle Wait çağrısı sonrasına geçilmesi için task1' içerisinde alt Task' ler tarafından yapılan işlemler dışında kalanların bitirilmesinin yeterli olması istenebilir. Beta 1' de bu durumu gerçeklemek için TaskCreationOptions.DetachedFromParent enum sabiti değerinde yararlanılmaktaydı(ki Beta 2' de kaldırıldı). Ancak Beta 2 sürümünde durum değiştirildi.

Beta 2 sürümüne göre varsayılan olarak alt Task nesne örnekleri içerisinde yer aldıkları üst Task' lere bağlı değildir(Varsayılan olarak Detached). Açıkçası, varsayılan olarak Task' lerin Detached olarak tesis edildiklerini ifade edebiliriz. Buna göre yukarıdaki kodun çalışma zamanı çıktısı Beta 2 sürümü için aşağıda görüldüğü gibi olacaktır.

 

Dikkat edileceği üzere task1 ile aynı zaman dilimi içerisinde task2 başlatılmış ancak task2 nin tamamlanması beklenmeden task1' deki işlemler bittiği için program sonuna gelinmiştir. Nitekim task2 varsayılan olarak task1' e bağlanmadığından(Detached) task1.Wait çağrısı gerçekten sadece task1' in işleyişinin tamamlanması ile ilgilenmiştir. Peki task2' nin task1' e bağlanması için ne yapılmalıdır? Bu amaçla task2 nesne örneğinin üretildiği yerde TaskCreationOptions.AttachedToParent enum sabiti değerinin kullanılması gerekmektedir. Dolayısıyla kodu aşağıdaki gibi güncellememiz yeterli olacaktır.

Task task1 = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Task 1 başlangıç zamanı {0}",DateTime.Now.ToLongTimeString());
                Task task2 = Task.Factory.StartNew(() =>
                    {
                        Console.WriteLine("Task 2 başlangıç zamanı {0}", DateTime.Now.ToLongTimeString());
                        Thread.Sleep(6000);
                    },TaskCreationOptions.AttachedToParent
                );
                Thread.Sleep(3000);
            }
            );

Kodun bu şekilde çalıştırılması sonucu aşağıdaki ekran çıktısı ile karşılaşılacaktır. 

AttachedToParent değeri nedeni ile task2, task1' e bağlı hale getirilmiştir. Yani task1, task2' nin parent Task' i olarak belirlenmiştir. Bu durumda task1.Wait çağrısının yapıldığı noktada Attach edilmiş tüm Task referansları değerlendirileceğinden task1' inde tamamlanması beklenilmiştir. Bu durum her iki çalışmadaki Program Sonu sürelerinden anlaşılabilmektedir. Nitekim ilk çalışmada task2 içerisindeki 6 saniyelik Sleep çağrısı hesaba katılmazken, ikinci örnekte katılmıştır. Task Parallel Library ile ilişkili olarak Beta 2' de gelen diğer değişiklikleri ele aldığımız başka bir yazımızda görüşmek dileğiyle, hepinize mutlu günler dilerim.

Tags:  
Categories:   TPL
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

Task Parallel Library(TPL) - İptal İşlemi [Beta 2]

Perşembe, 12 Kasım 2009 11:00 by bsenyurt

Merhaba Arkadaşlar,

Uzun süredir .Net Framework 4.0' ın bir parçası olarak gelen paralel programlama alt yapısı ile uğraşmıyordum. En son Beta 1 sürümündeyken Task Parallel Library ve PLINQ ile ilişkili konulara bakma fırsatım olmuştu. Zaman ilerledi ve .Net Framework 4.0 Beta 2 sürümü yayınlandı. Bu sürümde Beta 1' e göre bazı farklılıklar bulunmakta. Yani farklılıkları yeniden öğrenme aşamasına gelmiş durumdayız. Bunu WF tarafında, WCF tarafında gördüğümüz gibi halen gelişmekte olan Paralel programlama alt yapısında da görmekteyiz. İşte bu günkü yazımızda herhangibir Task' in iptal sürecinin Beta 2 sürümünde nasıl değerlendirildiğini incelemeye çalışıyoruz olacağız. Beta 1 sürümünde bir Task' in iptal edilmesi için aşağıdaki kod tasarımı kullanılmaktaydı.

Task parallelTask= Task.Factory.StartNew(() =>
{
 for (; ; )
 {
  if (Task.Current.IsCancellationRequested)
  {
   Task.Current.AcknowledgeCancellation();
   return;
  }
  //TODO: Gerekli işlemler
 }
});

ve kodun herhangibir yerinde Task' in iptal edilmesi için ilgili Task nesne örneği üzerinden Cancel metodu çağırılmaktaydı.

parallelTask.Cancel();

Bu modelde bir Task' in iptal istemi geldiğinde, çalışmakta olan güncel Task' in üzerinden IsCancellationRequested özelliğinin değerine bakılması gerekmekteydi. Eğer bu özelliğin değeri true ise bu durumda ilgili Task' in iptal işlemi ile ilişkili olaraktan bilgilendirilmesi amacıyla AcknowledgeCancellation metodu çağırılmakta ve örneğin return gibi bir çağrı ile paralel yüreyen operasyondan çıkılmaktaydı. Ancak Beta 1 sürümündeki bu yaklaşımın bazı handikapları da bulunmaktaydı. Söz gelimi iptal isteğinin kontrolü sırasında yürüyen süreci kesmek için return gibi bir çıkış kullanılması, Task tipi üzerinden static Current özelliğne gidilmek zorunda kalınması, iptal ile ilişkili tüm fonksiyonellik ve özelliklerin Task tipi üzerinde yer alması vb...Bu sebeplerden dolayı Beta 2 sürümünde bir Task' in iptal edilme işlemi yeniden değerlendirelerek daha tutarlı bir hale getirildi. Buna göre yeni modeli aşağıdaki örnekte görüldüğü üzere özetleyebiliriz.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TaskParallelLibrary
{
    public partial class Form1 : Form
    {
        CancellationTokenSource source;
        CancellationToken token;
        Task parallelTask;

        public Form1()
        {
            InitializeComponent();

            source = new CancellationTokenSource();
            token = source.Token;
        }

        private void btnStart_Click(object sender, EventArgs e)
        {
            parallelTask = Task.Factory.StartNew(() =>
            {
                for (int i = 1000; i < 1000000; i++)
                {
                    // Eğer Cancel isteği gelirse OperationCancelled istisnası fırlatılır.
                    token.ThrowIfCancellationRequested();
                    // Burada bir takım işlemler yapılmakta olduğunu düşünebiliriz
                }
            }
            , token
            ); // StarNew metodunda kullanılan ikinci parametre ile CancellationToken referansının aktarıldığında dikkat edelim.
        }

        private void btnCancel_Click(object sender, EventArgs e)
        {
            source.Cancel(); // İptal işlemi için Task referansı yerine CancellationTokenSource referansı kullanılmaktadır.
        }
    }
}

Beta 2 ile gelen iptal modelinde CancellationTokenSource, CancellationToken isimli yeni tiplerin kullanıldığı görülmektedir. Dikkat çekici noktalardan birisi, Task' in başlatılması işlemi sırasında ikinci parametre olarak bir CancellationToken referansının gönderilmesidir. Bu referanstan yararlanılarak for döngüsü içerisinde ThrowIfCancellationRequested metodu çağırılmaktadır. İşte bir yenilik daha. Bu metod, token referansı ile ilişkilendirilmiş olan Task' e bir iptal isteği gelip gelmediğini kontrol etmektedir. Eğer böyle bir istek gelmişse OperationCancelledException tipinden olan istisnayı fırlatmakta ve çalışmakta olan Task' in iptal edilmesine neden olmaktadır. Bir önceki modelde görüldüğü gibi bir if kontrolü yapılmasına ve return gibi bir çıkış yolu kullanılmasına gerek kalmamaktadır. Bir diğer önemli noktada, iptal işlemi için Task referansı yerine CancellationTokenSource nesne örneğinin kullanılıyor olmasıdır.

İptal işlemi ile ilişkili üyelerin tamamı Task tipinden çıkartılmıştır(Cancel,AcknowledgeCancellation,IsCancellationRequested,CurrentTask vb...) Cancel çağrısı için CancellationTokenSource , ThrowIfCancellationRequested çağrısı ile iptal kontrolü ve operasyonun kesilmesi için CancellationToken referanslarının kullanıldığına dikkat edelim. Buna göre bir iptal işlemi, aynı CancellationToken referansını kullanan birden fazla Task' ede uygulanabilir. Zaten bu amaçla, StartNew gibi bazı metodların yeni aşırı yüklenmiş versiyonlarına CancellationToken referansının taşınabiliyor olması sağlanmıştır.

Bakalım Beta 2 sürümüne göre paralel programlama alt yapısında başka ne gibi yenilikler bulunmaktadır. Bunları ilerleyen yazılarımızda incelemeye devam ediyor olacağız. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Tags:  
Categories:   TPL
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

for mu, foreach mi? Yoksa Parallel.For mu, Parallel.ForEach mi? [Beta 1]

Çarşamba, 10 Haziran 2009 01:51 by bsenyurt

Merhaba Arkadaşlar,

Gecenin bu saatinde uyuyamayıp blog' uma bir şeyler yazmak isteyişimin sebebi, bu gün bir okurumdan gelen şu sorudur; "Madem Parallel.For veya Parallel.ForEach ile herşey daha hızlı oluyor, niye normal for ve foreach döngülerini bu formasyona sokmuyorlarda ek bir şeyler ilave ediyorlar". Dolayısıyla klavyemi elime aldım ve hemen bir test programı yazmaya koyuldum. Bu kez amaç vaat edilenin tersini göstermekti. Yani performansa ve hıza ulaşmaya çalışmayacak, tam aksi yöne gitmeye gayret edecektim. Aslında bu işlemler için gayet profesyonel test araçları mevcuttur. Ancak bir araca gerek duymadanda analizimizi yapabiliriz. İşe Visual Studio 2010 Beta 1 üzerinde, basit bir Console örneği geliştirerek başladım. İşte kodlarımız;

using System;
using System.Diagnostics;
using System.Threading;

namespace ForForEachPerformance
{
    class Program
    {
        static void Main(string[] args)
        {
            int arraySize = 1000;
            double[] array1 = new double[arraySize];
           
            Random rnd=new Random();
            Stopwatch watch1 = Stopwatch.StartNew();

            for (int i = 0; i < array1.Length; i++)
            {
                array1[i] = (rnd.NextDouble()/Math.Cos(rnd.NextDouble()))*Math.Sqrt(rnd.NextDouble());
            }

            Console.WriteLine("For döngüsü eleman ekleme süresi {0} milisaniyedir.",watch1.Elapsed.TotalMilliseconds.ToString());

            double[] array2 = new double[arraySize];

            Stopwatch watch2 = Stopwatch.StartNew();
            Parallel.For(0, array2.Length, i =>
                {
                    array1[i] = (rnd.NextDouble() / Math.Cos(rnd.NextDouble())) * Math.Sqrt(rnd.NextDouble());
                }
            );

            Console.WriteLine("Parallel.For döngüsü eleman ekleme süresi {0} milisaniyedir.", watch2.Elapsed.TotalMilliseconds.ToString());

            Stopwatch watch3 = Stopwatch.StartNew();

            for (int i = 0; i < array1.Length; i++)
            {
                double d = array1[i];
            }

            Console.WriteLine("For döngüsü eleman okuma süresi {0} milisaniyedir.", watch3.Elapsed.TotalMilliseconds.ToString());

            Stopwatch watch4 = Stopwatch.StartNew();

            Parallel.For(0, array1.Length, i =>
                {
                    double d = array1[i];
                }
            );

            Console.WriteLine("Parallel.For döngüsü eleman okuma süresi {0} milisaniyedir.", watch4.Elapsed.TotalMilliseconds.ToString());

            Stopwatch watch5 = Stopwatch.StartNew();

            Parallel.ForEach(array1, i =>
                {
                    double d = i;
                }
            );

            Console.WriteLine("Parallel.ForEach döngüsü eleman okuma süresi {0} milisaniyedir.", watch5.Elapsed.TotalMilliseconds.ToString());

            Stopwatch watch6 = Stopwatch.StartNew();

            foreach (double i in array1)
            {
                double d = i;
            }

            Console.WriteLine("ForEach döngüsü eleman okuma süresi {0} milisaniyedir.", watch6.Elapsed.TotalMilliseconds.ToString());
            Console.ReadLine();
        }
    }
}

Aslında kodumuz son derece basit. Eleman sayısını 1000 olarak set ettiğimiz double tipinden dizilere eleman eklemek(ki eklerken işin uzun sürmesini sağlamak adına tamamen anlamsız bir matematik formülü içermektedir) ve okumak için for, foreach, Parallel.For ile Parallel.ForEach metodlarını kullanmaktayız. Burada eleman sayısının bilhakis düşük tutulması son derece önemlidir aslında. Program çalıştırıldığında, for ve Parallel.For ile yapılan ekleme işlemleri ile,  yine for, foreach ve Parallel.ForEach ile yapılan okuma işlemlerine ait toplam süre değerlerini bildirmektedir. Ben arka arkaya 10 deneme yaptıktan sonra ekleme işlemleri için aşağıdaki sonuçları elde ettim.

Eleman Ekleme
Deneme For ile Parallel.For ile
1 0,2592 4,6796
2 0,2718 3,8415
3 0,2799 3,913
4 0,2732 5,7423
5 0,2584 4,3159
6 0,291 3,8208
7 0,2782 3,9725
8 0,2612 4,041
9 0,2626 3,9412
10 0,2598 8,0166

Grafiksel olarak bakarsak çok daha acı bir gerçekle karşılabiliriz.

Görüldüğü üzere for ile gerçekleştirilen ekleme işlemi, eş zamanlı ve dolayısıyla paralel çalışabilen Parallel.For kullanımına göre çok daha hızlı yapılmıştır. Peki diziden veri okuma işlemi sırasındaki durum nedir? İşte sonuçlar;

Eleman Okuma
Deneme For ile Parallel.For For Each Parallel.ForEach
1 0,0128 0,6894 0,0145 23,1096
2 0,0125 0,8129 0,0134 26,3994
3 0,0128 0,7707 0,0139 25,152
4 0,0131 0,6062 0,0136 25,661
5 0,0128 0,8048 0,0139 25,6406
6 0,0131 0,842 0,0153 35,9978
7 0,0125 0,5439 0,0131 24,9442
8 0,0134 1,3035 0,0134 23,9636
9 0,0125 0,7635 0,0136 24,2947
10 0,0128 14,211 0,0142 37,5547

Okuma işlemlerinin grafiksel sonucu ise aşağıdaki şekilde görüldüğü gibidir.

Her ne kadar for süreleri grafik üzerinde görünmesede(sıfıra çok yakın olduğu için) en hızlı okuma süresi rekoru kendisine aittir. Sonrasında foreach gelmektedir. Parallel.For nispeten belirli süre foreach' e yakın değerlerde seyretmesine rağmen, 10ncu denemede açılan paralel task' lerin canından bezmesi nedeni ile çok kötü bir süre üretmiştir. Ancak Parallel.ForEach bu teste göre sondan birinci olmuştur.

Bu testleri Dual Core işlemcili e 4 Gb Ram' i olan, Vista yüklü bir sistem üzerinde denediğimde elde ettim. Elbetteki paralel tekniklerin burada kötü sonuçlar vermesinin en büyük nedeni işlemlerin zaten normal for veya foreach ' ler ile çok kısa sürede tamamlanabilmesidir. Öyleki, bu süreler içerisinde işleyişi paralel iş parçalarına ayırmak için yapılacak tüm hazırlıklar, sürenin dahada uzamasına neden olmaktadır. Sonuç olarak şu noktayı vurgulamak gerekiyor,

Parallel.For ve Parallel.ForEach metodları, döngü içerisindeki işlemlerin gerçekten uzun sürelerde yapılabilldiği durumlarda kullanılmalıdır. Bu noktada kodun çalışacağı sistemin kapasitesi veya döngüler içerisinde yer alan işlemlerin maliyeti gibi pek çok etken, tamamlanma sürelerinde belirleyici rol oynamaktadır. Söz gelimi grafik tabanlı matematiksel hesaplamaların çok sayıda nesne örneği için yapılması gereken durumlarda(DirectX, OpenGL kullanan grafik uygulamaları veya oyunlar) paralel tekniklerden yararlanılması düşünülebilir.

Bir diğer önemli noktada aslında, Parallel.For, Parallel.ForEach, Task, Parallel.Invoke gibi kavramların paralel programlama genişletmesi(Parallel Extensions) olaraktan henüz beta aşamasında yer alan bir ürüne dahil edilmiş olmalarıdır ki .Net Framework 3.5 ilede ek bir paket yüklenerek ele alınabilmektedir. Bu açıdan bakıldığında Relase sürümde farklılıklar olması veya arzu edilen iyileştirmelerin yapılmasıda muhtemeldir.

Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

ForForEachPerformance.rar (21,64 kb)

Tags:  
Categories:   TPL
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

TPL - İptal İşlemi [Beta 1]

Pazartesi, 8 Haziran 2009 22:19 by bsenyurt

Merhaba Arkadaşlar,

Bir önceki blog yazımda, TPL kullanılarak WinForms uygulamalarında paralel işlemlerin nasıl yapılabileceğini ele almaya çalışmıştım. Örnekte son geldiğimiz noktaya bakıldığında aşağıdaki kazanımları elde ettiğimizi düşünebiliriz.

  • Parallel.ForEach sayesinde resim dosyalarının iterasyonun daha hızlı gerçekleştirilebilmektedir.
  • WinForms tarafındaki Cross-Thread ihlalinin önüne geçilmiştir.
  • Task sınıfı üzerinden kullanılan StartNew metodu yardımıyla resim içeren Button kontrollerin üretildiği anda ekranda gösterilebilmesi sağlanmıştır.
  • Yine StartNew metodunun kullanımı sayesinde kullanıcının paralel işlemler devam ederken, Form üzerindeki diğer kontroller ile etkileşimi sağlanmıştır.

Ancak biraz durup düşündüğümde unuttuğum önemli bir nokta olduğunu farkettim. İşlemler her ne kadar kısa gibi görünsede, kullanıcı bir yerde iptal etmek isterse ne olacak. Undecided Dolayısıyla uygulamaya bir iptal sürecininde ekleniyor olması gerekmekte. Aslında bu işlem son derece basit. Nitekim Task sınıfının bu işlemler için tasarlanmış olan Cancel isimli bir metodu bulunmaktadır. Lakin örnek dikkatlice göz önüne alındığında, resim dosyları üzerindeki iterasyonun Parallel.ForEach yardımıyla gerçekleştirildiği görülür. Dolayısıyla Parallel.ForEach içerisinde hareket edilirken, iptal talebi gelip gelmediğinin kontrol edilmesi gerekmektedir. Ki buda tek başına yeterli değildir. Nitekim, Parallel.ForEach metoduda kendi içerisindeki işlemler için arka planda açtığı Task' leri kullanmaktadır. Dolayısıyla ForEach içeriğindeki task' lerinde iptal edilmesi gerekmektedir. Bu nedenle kod içeriğini aşağıdaki gibi değiştirmeliyiz.

private Task task1 = null;

        private void btnStart4_Click(object sender, EventArgs e)
        {
            flowLayoutPanel1.Controls.Clear();
            Stopwatch watch = Stopwatch.StartNew();

            task1=Task.Factory.StartNew(() => FillImages(null));

            watch.Stop();
            lblElapsedTime.Text = String.Format("İşlemler {0} saniyede bitmiştir.", watch.Elapsed.TotalSeconds.ToString());
        }

        private void btnCancel_Click(object sender, EventArgs e)
        {
            if (task1 != null)
                task1.Cancel();
        }

        private void FillImages(object state)
        {
            Task currentTask = Task.Current;

            Parallel.ForEach(Directory.GetFiles(imagesPath), (f, ls) =>
            {
                if (currentTask.IsCancellationRequested)
                {
                    ls.Stop();
                    return;
                }
                FileInfo fInfo = new FileInfo(f);
                if (fInfo.Length <= 1024 * 100
                    && fInfo.Extension == ".jpg")
                {
                    Thread.Sleep(100); // Bunu koymadığımızda UI istediğimiz gibi reaksiyon vermiyor.
                    Button btn = new Button();
                    btn.Width = 64;
                    btn.Height = 48;
                    btn.BackgroundImageLayout = ImageLayout.Stretch;
                    btn.BackgroundImage = Image.FromFile(f);
                    AddToPanel(btn);
                }
            }
            );
        }

Herşeyden önce iptal edilmek istenen Task sınıfına ait nesne örneğinin(task1 isimli değişken) sınıf seviyesinde bir değişken olarak ele alındığı görülmektedir. Kullanıcı iptal işlemi için Button kontrolüne tıkladığında, task1 değişkeni üzerinden Cancel metodu çağırılmaktadır. Bu durumda çalışma zamanında ForEach döngüsü içerisinde yer alan IsCancellationRequested özelliği true değeri döndürecektir. Bu özelliğe ulaşmak için Task sınıfının Current özelliğinden yararlandığımıza dikkat edilmelidir. Bu sayede ForEach içerisinde Parent Task örneğine ulaşılabilmektedir. Ardında ilginç bir kod parçası gelmektedir. ls isimli bir değişken üzerinden Stop metodu çağırılmıştır. İşte bu metod ForEach tarafından açılan task' lerin iptal edilmesini sağlamaktadır. Aslında Parallel sınıfı içerisinde ForEach veya For metodları içerisinde kullanılan temsilcilere bakıldığında ParallelLoopState isimli bir sınıf kullanıldığı görülür. Örneğin,

public static ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source, Action<TSource, ParallelLoopState> body);

Bu sınıf içerisinde Stop, Break gibi paralel döngünün durdurulması veya dışına çıkılması için gerekli metodlar yer almaktadır. Örneğimizde Action temsilcisinin kullanıdığı ikinci generic parametre, çalışma zamanındaki ParallelLoopState nesne örneğine denk gelmektedir. Ve sonuç...

Tekrardan görüşmek dileğiyle hepinize mutlu günler dilerim.

Tags:  
Categories:   TPL
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

TPL ile WinForms Macerası [Beta 1]

Pazar, 7 Haziran 2009 20:53 by bsenyurt

Merhaba Arkadaşlar,

Dün gece Task Parallel Library ile ilgili olarak internette araştırma yaparken, örnekleri çoğunlukla(hatta tamamen) Console uygulamaları üzerinde geliştirdiğimi farkettim. Oysaki TPL veya PLINQ gibi alt yapıların, WinForms yada WPF(Windows Presentation Foundation) uygulamalarında nasıl kullanılabileceğide önemli bir konuydu. Özellikle Windows Form' larının TPL çalışmalarına karşı nasıl tepkilerde bulunabileceği belkide en önemli noktaydı. Biliyorsunuz TPL alt yapısında, işlemci ve çekirdek gücü sonuna kadar kullanılmakta ve arka planda coşan pek çok Thread yer almaktadır. Fakat WinForms uygulamalarında herşeyin hakimi olan ana Thread' in genellikle bencil olduğuda bilinmektedir. Bu nedenle TPL ile çekilen bir veri içeriğinin, Form üzerindeki bir kontrole doldurulması gerçekten başa bela olabilir.Sealed İşte bu düşünceler içerisinde yola çıktım ve örnek bir senaryo üzerinde durmaya çalıştım.

İlk olarak senaryodan biraz bahsedeyim; bilgisayarımda resimlerin tutulduğu klasörde yer alan jpg dosyalarından 100 KB' ın altında olanları bulup, Form üzerindeki bir FlowLayoutPanel içerisinde Button bileşenleri ile göstermek istemekteyim. Kabaca aşağıdaki ekran görüntüsünde yer alan sonuçları elde etmek istediğimizi düşünebiliriz.

İşe ilk olarak eski stilde başladım. Yani tek bir Thread ile resimleri doldurmayı denedim. Bunun için kod içeriğini ilk etapta aşağıdaki gibi geliştirdim.

using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TPLAntrenmanlari2
{
    public partial class Form1 : Form
    {
        private string imagesPath = @"C:\Users\Burak Selim Senyurt\Pictures";

        public Form1()
        {
            InitializeComponent();
        }

        private void btnStart_Click(object sender, EventArgs e)
        {           
            flowLayoutPanel1.Controls.Clear();

            #region Single Thread Kullanılarak

            Stopwatch watch = Stopwatch.StartNew();

            foreach (string f in Directory.GetFiles(imagesPath))
            {
                FileInfo fInfo = new FileInfo(f);
                if (fInfo.Length <= 1024 * 100
                    && fInfo.Extension == ".jpg")
                {
                    Button btn = new Button();
                    btn.Width = 64;
                    btn.Height = 48;
                    btn.BackgroundImageLayout = ImageLayout.Stretch;
                    btn.BackgroundImage = Image.FromFile(f);
                    flowLayoutPanel1.Controls.Add(btn);
                }
            }

            watch.Stop();
            lblElapsedTime.Text = String.Format("İşlemler {0} saniyede bitmiştir.", watch.Elapsed.TotalSeconds.ToString());

            #endregion
        }
       
    }
}

İlk geliştirmede, resimlerin tutulduğu klasördeki dosyalar bir foreach döngüsü yardımıyla dolaşılmaktadır. Sonrasında ise uzantısı jpg olan ve 100 KB altında olanlar belirlenmektedir. Bu kritere uyan her bir resim için bir Button kontrolü üretilmekte ve arka plan olarak bulunan resim kullanılmakadır. Tabiki son olarak söz konusu Button kontrolü, FlowLayoutPanel bileşeni içerisine eklenmektedir. Sonuçlar benim sisteminde aşağıdaki gibi gerçekleşmiştir.

Dikkat çekici nokta işlemlerin tamamlanma süresidir. Neredeyse 20 saniye. Undecided Üstelik işlemler sırasında Form' u herhangibir yere çekiştiremediğimizi görürüz. Ayrıca, Button bileşenleri oluşturulup FlowLayoutPanel kontrolüne eklenirken Form üzerinde görsel bir hareketlilik olmadığı gözlemlenebilir. Ancak tüm işlemler bittikten sonra Button' ların görülmesi mümkün olacaktır. Tabiki isteklerimizden ilki işlemlerin daha kısa sürede bitirilmesi olarak düşünülebilir. Bu amaçla btnStart2_Click kodlarında Parallel.ForEach kullanımını tercih ettim. İşte kodun yeni hali;

private void btnStart2_Click(object sender, EventArgs e)
        {
            flowLayoutPanel1.Controls.Clear();

            #region Parallel.ForEach kullanımı

            Stopwatch watch = Stopwatch.StartNew();

            Parallel.ForEach(Directory.GetFiles(imagesPath), f =>
            {
                FileInfo fInfo = new FileInfo(f);
                if (fInfo.Length <= 1024 * 100
                    && fInfo.Extension == ".jpg")
                {
                    Button btn = new Button();
                    btn.Width = 64;
                    btn.Height = 48;
                    btn.BackgroundImageLayout = ImageLayout.Stretch;
                    btn.BackgroundImage = Image.FromFile(f);
                   
                    flowLayoutPanel1.Controls.Add(btn); // Exception: Cross-thread operation not valid: Control 'flowLayoutPanel1' accessed from a thread other than the thread it was created on.
                }
            }
            );

            watch.Stop();
            lblElapsedTime.Text = String.Format("İşlemler {0} saniyede bitmiştir.", watch.Elapsed.TotalSeconds.ToString());

            #endregion
        } 

Görüldüğü gibi tek fark Parallel.ForEach kullanımıdır. Bu sayede, ForEach içerisinde yer alan işlemlerin paralel iş parçalarına bölünerek gerçekleştirilmesi mümkün olacaktı. Ancak ortam Console değildi. Artık WinForms ortamındaydık. Çevresel faktörler daha farklıydı. Dolayısıyla sonuç aşağıdaki gibi oldu.

İşte beklenen hayalet. Sealed Durumu şu şekilde açıklayabiliriz. Windows uygulaması çalıştırıldığıda yürümekte olan ana Thread, kendisini Form üzerindeki tüm kontrollerin sahibi olarak ilan etmiştir. Bu nedenle farklı bir Thread içerisinden, sahibi olduğu bir kontrole ulaşılmasına izin vermez. Çözüm için pek çok farklı yol vardır. Ben bu yollardan birisi olan Invoker' lardan faydalanmaya karar verdim. İşte kodun son hali.

private void btnStart2_Click(object sender, EventArgs e)
        {
            flowLayoutPanel1.Controls.Clear();

            #region Parallel.ForEach kullanımı

            Stopwatch watch = Stopwatch.StartNew();

            Parallel.ForEach(Directory.GetFiles(imagesPath), f =>
            {
                FileInfo fInfo = new FileInfo(f);
                if (fInfo.Length <= 1024 * 100
                    && fInfo.Extension == ".jpg")
                {
                    Button btn = new Button();
                    btn.Width = 64;
                    btn.Height = 48;
                    btn.BackgroundImageLayout = ImageLayout.Stretch;
                    btn.BackgroundImage = Image.FromFile(f);
                    AddToPanel(btn);
                    // flowLayoutPanel1.Controls.Add(btn); // Exception: Cross-thread operation not valid: Control 'flowLayoutPanel1' accessed from a thread other than the thread it was created on.
                }
            }
            );

            watch.Stop();
            lblElapsedTime.Text = String.Format("İşlemler {0} saniyede bitmiştir.", watch.Elapsed.TotalSeconds.ToString());

            #endregion
        }

        #region  Cross-thread operation not valid hatasına karşı mücadele

        private delegate void AddControlHandler(Button pb);
        private void AddToPanel(Button pb)
        {
            if (flowLayoutPanel1.InvokeRequired)
                flowLayoutPanel1.BeginInvoke(new AddControlHandler(RealAddToPanel), new object[] { pb });
            else
                RealAddToPanel(pb);
        }
        private void RealAddToPanel(Button pb)
        {
            flowLayoutPanel1.Controls.Add(pb);
        }

        #endregion

Bu durumda kendi sistemimde aşağıdaki sonuçlar ile karşılaştığımı gördüm.

Evett...Durumu bir değerlendirelim. 20 saniyelik sürelerden yaklaşık 8 saniyelik sürelere indik. Bu çift çekirdekli bir sistem için iyi bir sonuç olarak görünüyor. (Tabi kodu daha fazla çekirdek sayısı bir sistemde ne yazıkki test edemedim. Ama siz değerli okurlarımdan test etme fırsatı olan olursa sonuçları paylaşmasını rica edeceğim.) Yinede herşey istediğimiz gibi değildir. Süre azalmasına rağmen, Form' u işlemler sırasında harekete ettiremediğimizi görürüz. Benzer şekilde resimleri içeren Button kontrolleri yine üretildikçe değil tüm işlemler bittikten sonra bir anda ekranda gösterilmektedir. Dolayısıyla Parallel.ForEach' in tam anlamıyla yeterli gelmediğini söyleyebiliriz. Çözüm olarak ThreadPool sınıfından yararlanabiliriz aslında. Şimdi kodu aşağıdaki gibi değiştirdiğimizi düşünelim.

#region  Cross-thread operation not valid hatasına karşı mücadele

        private delegate void AddControlHandler(Button pb);
        private void AddToPanel(Button pb)
        {
            if (flowLayoutPanel1.InvokeRequired)
                flowLayoutPanel1.BeginInvoke(new AddControlHandler(RealAddToPanel), new object[] { pb });
            else
                RealAddToPanel(pb);
        }
        private void RealAddToPanel(Button pb)
        {
            flowLayoutPanel1.Controls.Add(pb);
        }

        #endregion

        private void FillImages(object state)
        {
            Parallel.ForEach(Directory.GetFiles(imagesPath), f =>
            {
                FileInfo fInfo = new FileInfo(f);
                if (fInfo.Length <= 1024 * 100
                    && fInfo.Extension == ".jpg")
                {
                    Thread.Sleep(100); // Bunu koymadığımızda UI istediğimiz gibi reaksiyon vermiyor.
                    Button btn = new Button();
                    btn.Width = 64;
                    btn.Height = 48;
                    btn.BackgroundImageLayout = ImageLayout.Stretch;
                    btn.BackgroundImage = Image.FromFile(f);
                    AddToPanel(btn);
                }
            }
            );
        }
       
        private void btnStart3_Click(object sender, EventArgs e)
        {
            flowLayoutPanel1.Controls.Clear();

            #region Parallel.ForEach kullanımı

            Stopwatch watch = Stopwatch.StartNew();

            ThreadPool.QueueUserWorkItem(new WaitCallback(FillImages));

            watch.Stop();
            lblElapsedTime.Text = String.Format("İşlemler {0} saniyede bitmiştir.", watch.Elapsed.TotalSeconds.ToString());

            #endregion
        }

QueueUserWorkItem metodu parametre olarak WaitCallback temsilcisini kullanmaktadır. Bu temsilci ise FillImages metodunu işaret etmektedir. İşlemler FillImages metodu içerisinde yapılmaktadır. Bu durumsa sonuçlar çok daha ilginç olacaktır. Button kontrolleri oluşturuldukça FlowLayoutPanel kontrolü içerisindede görünür hale gelecektir. Ayrıca, Form' u işlmeler sırasında sürükleyebildiğimizi veya oluşturulan Button' lara tıklayabildiğimizide görebiliriz.

Ancak zaman ilerlemiştir ve artık Task sınıfı ve üyeleri ile aynı işlemi nasıl gerçekleştirebileceğimize bakmamız gerekmektedir. Sonuç itibaryle .Net 4.0 için aynı işleyişi aşağıdaki kod parçası ile gerçekleştirebiliriz.

private void btnStart4_Click(object sender, EventArgs e)
        {
            flowLayoutPanel1.Controls.Clear();
            Stopwatch watch = Stopwatch.StartNew();

            Task.Factory.StartNew(() => FillImages(null));

            watch.Stop();
            lblElapsedTime.Text = String.Format("İşlemler {0} saniyede bitmiştir.", watch.Elapsed.TotalSeconds.ToString());
        }

Bir önceki yazımızdan hatırlayacağınız gibi Task sınıfı üzerinden StartNew metodunu kullanarak paralel görevlerin başlatılması sağlanabilmektedir. Burada metoda parametre olarak Action temsilcisinin işaret edebileceği FillImage fonksiyonu verilmiştir. Sonuçlar yine yukarıdaki Flash animasyonundakine benzer olacaktır. Kullanıcılar, resimleri gösteren Button kontrolleri yüklenirken, Formun diğer alanları ile etkileşimde bulunubilmektedir. Ayrıca Button bileşenleri oluşturuldukça FlowLayoutPanel içerisinde görülebilmektedir. Ancak kod içerisinde küçük bir hile yaptığımı belirtmek isterim. Embarassed Dikkat ederseniz FillImages metodu içerisinde o anki Thread için 100 milisaniye kadar bir duraksatma yapılmaktadır. Bu yapılmadığı takdirde Button bileşenlerinin oluşturuldukça FlowLayoutPanel içerisinde gösterilmelerinde bir sıkıntı olduğu gözlemlenir. Açıkçası Tutarsız bir çalışma olmaktadır. Ancak şimdilik bu gecikmenin olmasında bir sakınca yoktur. Nitekim, kullanıcı zaten paralel süreç içerisindeki işlemlerden her biri bittikçe, tamamlanan o işi ele alabilmektedir. Yani işlemlerin tamamalanmasının beklenmesine gerek kalınmadan çalışmaya devam edilebilmektedir.

Böylece geldik bir blog yazımızın daha sonuna. İlerleyen dönemlerde aynı senaryoyu bir WPF uygulaması için ele almaya çalışıyor olacağım. Görüşmek dileğiyle.

TPLAntrenmanlari2.rar (40,63 kb)

Tags:  
Categories:   TPL
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

TPL için Önemli Bir Kavram : Task [Beta 1]

Cuma, 5 Haziran 2009 04:05 by bsenyurt

Merhaba Arkadaşlar,

Bir önceki blog yazımda Task Parallel Library alt yapısının ne olduğunu sizlere aktarmaya çalışmıştım. Tabiki bu alt yapı üzerinde durulması gereken pek çok konu bulunmaktadır. Heyecanım çok, anlatmak içinde sabırsızlanıyorum. Ama her zamanki gibi adım adım ilerlemekte ve acele etmemekte yarar olduğu kansındayın. TPL ile ilişkili önemli konulardan birisi Task(yada Task<T>) sınıfıdır. TPL esas itibariyle görev adı verilen küçük iş parçaları üzerine kurulu bir yapı olarak düşünülebilir. Bu nedenle Task sınıfı son derece önemlidir. Nitekim görevlerin yönetimli kod tarafındaki ifadesidir. Bu sınıf yardımıyla, paralel çalışacak olan görevlerin başlatılması, iptal edilmesi, bekletilmesi, arka arkaya eklenerek bir süreç tesis edilmesi gibi pek çok işlem yapılabilir. Task sınıfı normal şartlarda geriye değer döndürmeyen fonksiyonelliklerin eş zamanlı olarak çalıştırılmasında ele alınmaktadır. Geriye değer döndüren metodlar söz konusu olduğunda ise, Task<T> generic tipinden yararlanılabilir. Buradaki T, paralel çalışan metodun dönüş tipi olarak düşünülebilir. Aşağıdaki sınıf diagramında söz konusu tipler ve üyeleri yer almaktadır.

Aslında Task ve Task<T> sınıflarının static Factory özelliği üzerinden gidildiğinde StartNew metodu yardımıyla görevlerin başlatılması sağlanmaktadır. Diğer yandan Task<T> sınıfının Result özelliği, geri dönüş tipini belirtmektedir. Ayrıca sınıf diagramındanda görüldüğü gibi Task<T> sınıfı, Task sınıfından türemektedir. Factory özellikleri, TaskFactory veya TaskFactory<T> tipinden referanslar barındırmaktadır. Bu tiplerin içeriği ise aşağıdaki şekilde görüldüğü gibidir.

Tüm tiplerde pek çok önemli üye bulunmaktadır. Bunların hemen hepsini zaman içerisinde ele almaya gayret edeceğiz, hiç merak etmeyin. Şimdi gelin Task ve Task<T> sınıflarını basit (ve her zamanki gibi tam anlamıyla gerçek hayat örneği olmayan Embarassed) bir örnek üzerinden ele almaya çalışalım. İlk olarak senaryomuzdan bahsedelim. Senaryomuza göre resim dosyalarına ait 3 farklı işlemin gerçekleştiği metodların eş zamanlı ve paralel olarak çalıştırılmasını sağlamayı hedefliyoruz. Buna göre bir klasörden,

  • Resim dosyalarının toplam boyutunun bulunması,
  • Resimler içerisinde bmp olanların kaç adet olduklarının tespit edilmesi,
  • Resimler içerisinde bmp olanların farklı bir klasöre kopylanması,

işlemlerini gerçekleştiren fonksiyonelliklerimiz bulunmakta.

Normal şartlar altında herkesin burada durup biraz düşünmesi gerekiyor. Elimizde .Net Parallel Extensions olmadığını varsayalım. Bu durumda ya Multi-Thread mimarisini kullanacağız, yada delegate(temsilci) tiplerinden yararlanarak asenkron erişim modellerini(Polling, Callback, WaitHandle, Event-Based) ele alacağız. Bunu bir düşünün ve senaryoyu bu materyaller ile yazmayı bir deneyin. Wink

Tabi şunu biliyoruzki TPL alt yapısı, paralel işlemleri kolayca ele almamızı sağlayacak şekilde tasarlanmıştır. İlk etapta kodlarımızı aşağıdali gibi geliştirdiğimiz varsayalım.(Kodlarımızı Visual Studio 2010 Beta 1 üzerinde geliştirdiğimizi hatırlatayım)

using System;
using System.Configuration;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace HelloTasks
{
    class Program
    {
        static string imagesPath = ConfigurationManager.AppSettings["ImagesPath"];

        static void Main(string[] args)
        {
            long totalSize = GetTotalSize();
            int bmpCount = GetBmpCount();
            CopyBmp();
            Console.WriteLine("Toplam boyut {0} byte\nBmp sayısı {1}", totalSize.ToString(), bmpCount.ToString());

            Console.WriteLine("Devam etmek için bir tuşa basınız");
            Console.Read();
        }

        static long GetTotalSize()
        {
            Console.WriteLine("\t GetTotalSize metodu için Managed Thread Id {0}. Zaman {1}",Thread.CurrentThread.ManagedThreadId.ToString(),DateTime.Now.ToLongTimeString());
            string[] files=Directory.GetFiles(imagesPath);
            long totalSize = 0;
            foreach (string file in files)
            {
                FileInfo fInfo = new FileInfo(file);
                totalSize += fInfo.Length;
                Thread.Sleep(10); // işlemleri biraz geciktirmek için bilinçli olarak konulmuştur
            }
            return totalSize;
        }

        static int GetBmpCount()
        {
            Console.WriteLine("\t GetBmpCount metodu için Managed Thread Id {0} Zaman {1}", Thread.CurrentThread.ManagedThreadId.ToString(), DateTime.Now.ToLongTimeString());
            int result = 0;

            foreach (string file in Directory.GetFiles(imagesPath))
            {
                FileInfo fInfo = new FileInfo(file);
                Thread.Sleep(10); // işlemleri biraz geciktirmek için bilinçli olarak konulmuştur
                if (fInfo.Extension.Contains("bmp"))
                    result++;
            }

            return result;
        }

        static void CopyBmp()
        {
            Console.WriteLine("\t CopyBmp metodu için Managed Thread Id {0} Zaman {1}", Thread.CurrentThread.ManagedThreadId.ToString(), DateTime.Now.ToLongTimeString());
            foreach (string file in Directory.GetFiles(imagesPath))
            {
                FileInfo fInfo = new FileInfo(file);
                if (fInfo.Extension.Contains("bmp"))
                {
                    File.Copy(file, "C:\\Bitmaps\\"+ fInfo.Name,true);
                }
            }
            Console.WriteLine("Kopyalama işlemi tamamlandı");
        }
    }
}

Şunu hemen belirteyim; aslında Directory ve FileInfo sınıflarının söz konusu hesaplamalar için kolaylaştırıcı metodları zaten mevcut. Söz gelimi Directory sınıfının GetFiles metoduna filtre uygulayarak zaten bmp dosyalarını kolayca elde edebiliriz. Yada bmp dosyalarını ele alırken kopyalama işlemlerinide yapabiliriz. Ancak yazının başında da bahsettiğim üzere bu sadece örnek bir senaryo malzemesi. Önemli olan nokta GetTotalSize, GetBmpCount ve CopyBmp metodlarının paralel olarak çalıştırılmalarını sağlamak. Tabi şu andaki kod parçamız bu metodları ardışık(Sequential) olarak çalıştırmaktadır. Uygulamanın çalışma zamanı çıktısına baktığımızda ise aşağıdaki ekran görütüsündekine benzer sonuçları alırız.

Sanıyorumki metodların başlangıç zamanları ve aralarındaki farklar dikkatinizi çekmiştir. Bu zaten beklediğimiz bir sonuçtur aslında. Nitekim metod görevlerinin paralel olarak ele alınması için hiç bir şey yapmadık. Kodu paralel programlama felsefesine taşımak için aşağıdaki gibi değiştirmemiz gerekmektedir.

static void Main(string[] args)
        {
            Task[] tasks =
            {
                Task<long>.Factory.StartNew(GetTotalSize),
                Task<int>.Factory.StartNew(GetBmpCount),
                Task.Factory.StartNew(CopyBmp)
            };

            /* tasks isimli dizi içerisindeki Task<T> tipleri aynı generic tip ile kullanılmadıklarında Task<T>[] gibi bir dizi üretilememiş bu nedenle 0 ve 1nci indislerdeki Task tiplerinin Result özelliklerine ulaşabilmek için bilinçli olarak Task<T> tiplerine dönüşüm yapılmıştır. */
            Console.WriteLine("Toplam boyut {0} byte\nBmp sayısı {1}"
                , ((Task<long>)tasks[0]).Result.ToString()
                , ((Task<int>)tasks[1]).Result.ToString()
                );

            Console.WriteLine("Devam etmek için bir tuşa basınız");
            Console.Read();
        }

İlk olarak Task tipinden bir dizi ürettildiğini görmekteyiz ki bir dizi kullanmanın bir zorunluluk olmadığını biraz sonra göreceğiz. Nihayetinde elimizde birden fazla görev var. Bu ilk kod denememizde, görevlerin tamamı bir dizi içerisinde toplanmaktadır. Dizinin her bir elemanının oluşturulması sırasında Factory özelliği üzerinden StartNew metodunun çağırıldığına dikkat edelim. Bu noktada parametre olarak belirtilen metodların, Task' ler diziye eklenirken çalıştırıldığını söyleyebiliriz. Kodun devam eden kısmında ise, generic Task tiplerinin çalıştırdığı metodlardan gelen dönüş değerleri ele alınmak istenmektedir. Dönüş değerlerinin ele alınması sırasında bilinçli olarak tür dönüşümü yapıldığına dikkat edilmelidir. Nitekim, dönüş değerleri ancak Task<T> generic sınıfının Result özellliği üzerinden ele alınabilmektedir.

NOT : Tabiki bazı senaryolarda, tüm görevler aynı dönüş tipine sahip olabilirler. Bu durumda dizinin Task<T> tipinden tasarlanmış olması halinde, dönüştürme işlemlerine gerek olmadan sonuçlar alınabilir. Nitekim dizi üzerinde hareket edecek basit bir for each döngüsünün ele alacağı her bir eleman Task<T> tipinde olacağından, zaten Result özelliklerine otomatikman ulaşılabilecektir.

Bu noktada şunu vurgulamaktada yarar var; bazı durumlarda paralel çalışan metodların işlemlerini tamamlamadan kodun devam etmesi istenmeyebilir. Bu durumdada Task sınıfının static WaitAll veya WaitAny gibi metodlarını kullanarak gereken bekletmeleri yapabiliriz. Örneğimizde buna gerek kalmamıştır. Çünkü generic Task tiplerinin işaret ettiği metodlara ait dönüş tipleri alınmak istendiğinden, uygulama kodu zaten o anda sonuç gelmediyse mecburen beklemede kalacaktır. Peki örneği çalıştırdığımızda nasıl bir sonuç alırız.

Mutlaka dikkatinizi çekmiştir; her metod için ayrı bir Managed Thread Id değeri üretilmektedir. Oysaki ardışık(Sequential) çalışan modelde tüm metodlar aynı Thread içerisinde ele alınmıştır. Bu, Thread bölünümünün de bir göstergesidir. Diğer taraftan, metodlar arası süre farklılıkları neredeyse sıfıra yakındır. Görüldüğü gibi gayet basit bir şekilde işlemleri paralel hale getirmeyi başardık. Kod ile ilişkili önemli bir noktayı daha vurgulamak isterim. Biraz önce bahsettiğimiz gibi, aynı dönüş tipine sahip metodların kullanıldığı senaryolarda Task dizilerini kullanmak daha mantıklıdır. Bu nedenle yukarıdaki senaryoda yer alan kodda dizi kullanımı şart değildir. Bir başka deyişle aynı amacı yerine getirden bir kod parçası, aşağıdaki şekilde olduğu gibi ele alınabilir.

Task<long> task1=Task<long>.Factory.StartNew(GetTotalSize);
Task<int> task2=Task<int>.Factory.StartNew(GetBmpCount);
Task task3=Task.Factory.StartNew(CopyBmp);

Console.WriteLine("Toplam boyut {0} byte\nBmp sayısı {1}"
                    , task1.Result.ToString()
                    , task2.Result.ToString()
                    );

Bu sefer GetTotalSize ve GetBmpCount metodlarını kullanan Task tiplerine ait çalışma zamanı referansları, birer değişkene atanarak kullanılmışlardır. Bu durumda bir önceki örnekte yaptığımız gibi Result özelliğine erişmek için, cast işlemi yapılmasına da gerek kalmamaktadır ki bu oldukça doğru bir yoldur. Dolayısıyla kodu daha düzgün bir hale getirmiş bulunuyoruz. Sizlerde Task ve Task<T> sınıflarını kullanarak bir kaç antrenman yapmayı deneyebilirsiniz.

NOT : Visual Studio 2010 ile birlikte gelen Parallel Tasks ve Parallel Stacks debugger pencereleri yardımıyla çalışma zamanında, task ve thread' lerin durumunu daha net bir şekilde analiz edebilirsiniz. Bu konuyu bir görsel dersimizde ele almaya çalışacağım.

Tabiki konuyu daha derinlere genişletmek mümkündür. Örneğin bazı görevlerin, kendinden önceki görev(ler) tamamlandıktan sonra başlatılması istenebilir. İşte diğer blog yazımın konusunu şimdiden bulduk. Wink Tabi başımıza dert açacak daha pek çok konuda var. Söz gelimi, TPL alt yapısını WinForms yada WPF gibi uygulamalarda ele aldığımızda neler olacaktır kimbilir Undecided. Malum WinForms yada WPF ekranlarında, Main Thread bencillik edip ekran üzerindeki kontrolleri başka Thread' ler ile paylaşmak istemez. Bu bencilliğe ortak olduğumuzda WinForms tarafında Illegal Cross Thread istisnalarına düştüğümüzü gayet iyi biliyoruz. Bu ve benzeri diğer konuları ilerleyen zamanlarda irdelemeye devam ediyor olacağız. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

HelloTasks.rar (23,71 kb)

Tags:  
Categories:   TPL
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

TPL(Task Parallel Library) Nedir? [Beta 1]

Çarşamba, 3 Haziran 2009 11:50 by bsenyurt

Merhaba Arkadaşlar,

Uzun uzun zaman önceydi. İlk bilgisayarımı daha dün gibi hatırlıyorum. Efsane Commodore 64

Açıkçası onunla yaptığım tek şey oyun oynamaktı itiraf ediyorum. En çok sevdiğim oyunlar arasında Grean Beret, Barbarian, Karate Kid 2, 1942, Airwolf vardı. Gel zaman git zaman, Üniversite yıllarına girince, bilgisayar işini daha ciddi düşünmeye başlamıştım. Yanlış hatırlamıyorsam yaklaşık olarak 2400 dolar değerinde ( Money mouth) 486DX işlemcili bir bilgisayarım daha olmuştu.

Sonrada olayların ardı arkası kesilmedi ve Pentium MMX, Celeron derken, çift çekirdekli ve hatta şu sıralarda moda olan 4 çekirdekli işlemciler ile karşılaştık.

Özellikle işlemcilerin bu şekilde ilerlemesine paralel olarak, yazılım geliştirme ortamlarında da pekala pek çok değişiklik ve yenilikçi fikir ortaya çıktı. Son zamanların özellikle Microsoft .Net cephesindeki en popüler konularından biriside paralel genişletmeler(Parallel Extensions). Bir başka deyişle, sistemin sahip olduğu işlemci gücünün tümünü kullanarak(Arabanın hakkını ver hakkını Smile ), paralel işlemler veya eş zamanlı yürütmelerin gerçekleştirilmesi. Bildiğiniz gibi paralel genişletmelerin önemli kısımlarından birisi olan PLINQ(Parallel Language INtegrated Query) alt yapısı üzerine yaptığım araştırmalarımı ve edindiğim bilgileri bir süredir sizlerle paylaşmaktayım. İşte bu yazımızda diğer önemli parça olan (belkide ile etapta incelenmesi gereken) TPL(Task Parallel Library) alt yapısını incelemeye başlıyor olacağız.

TPL' in en büyük amacı, eş zamanlı veya paralel olarak yürütülmek istenen işlemlerin, daha kolay ve basit bir şekilde ele alınmasını sağlamaktır. Bu anlamda günümüz işlmecilerinin çekirdek sayısı veya sistemlerdeki işlemci sayısının birden fazla olması durumunda, TPL verimli sonuçlar elde etmemizi sağlamaktadır. Bu açıdan bakıldığında TPL alt yapısına tüm sistem çekirdek gücünü verme imkanına sahip olduğumuzu belirtebiliriz. Ancak elbetteki bu güç yanlış anlaşılmamalı ve kullanılmamalıdır. Bildiğiniz gibi "kontrolsüz güç, güç değildir" derler Wink

Elbetekki TPL kullanımı ile ilişkili olarak unutulmaması gereken bir noktada, işlemlerin Multi-Threading mantığına göre yapılıyor olmasıdır. Dolayısıyla, programın çalışma zamanı yükünü arttırıcı bir etkendir. Bir başka deyişle her işlemin, elimizde TPL var diye paralel olarak yürütülmeye çalışılması doğru değildir. Bazı süreçlerin gerçekten ve bilinçli olaraktan ardışık(Sequential) yürümesi gerekebilir.

NOT : Aslında, PLINQ(Parallel Language INtegrated Query) kendi alt yapısında TPL tipleri ve üyelerinden destek almaktadır.

Artık olaya biraz daha teknik açıdan bakabileceğimizi düşünüyorum. TPL esas itibariyle .Net Framework 4.0 ile birlikte gelen ve paralel işlem yapma yeteneklerini ele alan kütüphanedir. System.Threading ve System.Threading.Tasks isim alanları bu kütüphaneye ait çeşitli tipleri ve üyelerini içermektedir. TPL, içerisinde birde Task Scheduler içerir. Bu planlayıcı ThreadPool ile TPL tipleri arasındaki entegrasyonu sağlamaktadır. Ancak istenirse kendi özel görev planlayıcılarımızı yazabilir ve kullanabiliriz. Gerçi benim buna niyetim yok Embarassed Aşağıdaki şekilde özellikle System.Threading.Tasks isim alanı altında yer alan tipler görülmektedir.

Geliştirme sürecinde özellikle System.Threading.Tasks isim alanı altında yer alan tipler kullanılmaktadır. TPL kütüphanesinin belkide en önemli tipi Task sınıfıdır. Task sınıfına ait üyeler kullanılarak aşağıdaki işlemleri gerçekleştirebiliriz;

  • Yeni görev(ler) başlatılabilir, iptal edilebilir yada bekletilebilir.
  • Bir görevin tamamlanması halinde, tamamlandığı yerden başka bir görev veya görevlere çağrıda bulunulabilir.
  • Başlatılan görevlerden geriye değer döndürülebilir.
  • Bir görev kendi içinde alt görevler başlatabilir. Bu görevler aynı Thread içerisinde veya farklı bir Thread üzerinde çalışıyor olabilir.

Tüm teorik bilgiler bir yana, konuyu ilk etapta kavramının en kolay yolu basit bir örnek üzerinden ilerlemektir. Bu anlamda, TPL' e Hello World demenin belkide en kolay yolu, System.Threading isim alanı altında yer alan Parallel static sınıfı ve üyelerini kullanmaktır.

Görüldüğü gibi For, ForEach ve Invoke isimli bizlere çok tanıdık gelen metodlar yer almaktadır. Bu fonskiyonellikleri kullanarak işlemlerin paralel olarak yürütülmesi sağlanabilmektedir. For ve ForEach metodları adlarındanda anlaşılacağı üzere, koleksiyon veya dizi yapıları üzerinde döngüsel işlemlerin paralel olarak yürütülmesini sağlamaktadır. Invoke metodu ise sunduğu Action temsilcisi(delegate) yardımıyla, birden fazla metodun aynı anda paralel olarak çalıştırılabilmesine olanak sağlamaktadır. 

NOT : For veya ForEach gibi Parallel sınıfına ait üyeleri kullandığımız hallerdede, arka planda Task sınıfı ve üyeleri gizlice devreye girerler.

Aşağıdaki Console uygulamasında bu metodalara ait örnek kullanımlar yer almaktadır.

using System;
using System.Linq;
using System.Threading;

namespace HelloTPL
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] numbers = Enumerable.Range(1, 100000).ToArray();

            #region Parallel.For Örneği

            // Action temsilcisinin söylediği kurallara uygun olaraktan, lambda operatöründen yararlanılır.
            Console.WriteLine("For\n");
            Parallel.For(1, numbers.Length,
                i =>
                {
                    if(i%1500==0)
                        Console.Write("{0} ",i.ToString());
                }
            );
           
            Console.WriteLine("\n\nFor(İçeriden başka metod çağırarak)\n");           
            Parallel.For(1,numbers.Length,
                (i)=>{
                    if (i % 1500 == 0)
                        Task1(i);
                }
            );

            #endregion

            #region Parallel.ForEach Örneği

            /* ForEach metodunun 19 aşırı yüklenmiş versiyonu vardır. İlk dikkati çeken nokta, IEnumerable<T> generic arayüzünü(interface) implemente eden referanslarıda parametre olarak almasıdır. Dolayısıyla, her koleksiyon veya diziye uygulanabilir. */
            Console.WriteLine("\n\nForEach Örneği\n");
            Parallel.ForEach(numbers, number =>
            {
                if (number % 1500 == 0)
                    Console.Write("{0} ", number.ToString());
            }
            );

            #endregion

            #region Parallel.Invoke Örneği

            Console.WriteLine("\n\n");

            // Parallel.Invoke metodu Action temsilcisi tipinden referanslar alan bir diziyi parametre olarak kullanır.
            // Bu şekilde istenildiği kadar metodun paralel olarak başlatılması sağlanabilir           
            Parallel.Invoke(
                () =>
                {
                    Console.WriteLine("Toplam Tek sayı hesabı başladı\n");
                    Console.WriteLine("Managed Thread ID {0} ",Thread.CurrentThread.ManagedThreadId.ToString());
                    OddCount(numbers);
                    Console.WriteLine("Toplam Tek sayı bulma işi tamamlandı\n");
                },
                () =>
                {
                    Console.WriteLine("Toplam Çift sayı hesabı başladı\n");
                    EvenCount(numbers);
                    Console.WriteLine("Managed Thread ID {0} ", Thread.CurrentThread.ManagedThreadId.ToString());
                    Console.WriteLine("Toplam Çift sayı bulma işi tamamlandı\n");
                }
                ,
                () =>
                {
                    Console.WriteLine("9 ile bölünenlerin toplamını bulma işi başladı\n");
                    NineCount(numbers);
                    Console.WriteLine("Managed Thread ID {0} ", Thread.CurrentThread.ManagedThreadId.ToString());
                    Console.WriteLine("9 ile bölünenlerin toplamını bulma işi tamamlandı\n");
                }
        );

            #endregion
        }
       
        static void Task1(int number)
        {
            // Değişiklik işlemler
            Console.Write("{0} ", number.ToString());               
        }

        static void EvenCount(int[] numbers)
        {
            int result = (from number in numbers
                          where number % 2 == 0
                          select number).Count();
            Console.WriteLine("\tDizi içerisinde {0} adet ÇİFT sayı vardır\n",result.ToString());
        }
        static void OddCount(int[] numbers)
        {
            int result = (from number in numbers
                          where number % 2 != 0
                          select number).Count();
            Console.WriteLine("\tDizi içerisinde {0} adet TEK sayı vardır\n", result.ToString());
        }

        static void NineCount(int[] numbers)
        {
            int result = (from number in numbers
                          where number % 9 == 0
                          select number).Count();
            Console.WriteLine("\tDizi içerisinde {0} adet 9 ile bölünebilen sayı vardır\n", result.ToString());
        }
    }
}

Aslında kod son derece açıktır ancak dikkat edilmesi gereken noktalarda vardır. For, ForEach ve Invoke metodları, Action temsilcisini sıklıkla kullanmaktadır. Bunun en büyük nedeni, paralel işleme tabi tutulacak kod parçalarını taşıyan herhangibir metod veya bloğun kullanılabilmesini sağlamaktır. Bildiğiniz gibi Action temsilcisi, parametre almayan ve geriye döndürmeyen metodları işaret etmektedir. Diğer taraftan Func temsilcisinin kullanıldığı versiyonlarda bulunmaktadır. Yani geriye değer döndüren ve parametre alan metodlarında işin içerisinde katılması sağlanabilir. Elbette kullanımı kolaylaştırmak adına, lambda operatörüde(=>) ciddi şekilde ele alınmaktadır. Uygulamayı çalıştırdığımızda aşağıdaki ekran görüntüsünde yer alan sonuçlara benzer bir çıktı elde ederiz.

Tabiki kodu test ettiğimiz sistemin çekirdek veya işlemci sayısına göre, yada o anda çalışmakta olan programlara göre farklı sıralarda sonuçlar elde edilebilir. Ancak çalışan kod parçasında işlemlerin paralel yapıldığına dair pek çok iz vardır. Dikkatlice bakıldığında For, ForEach döngülerinin dizileri gerçekten paralel bir sırada değerlendirdiği ve işlemeri yaptığı ortadadır. Invoke metoduda benzer şekilde, çağırdığı 3 metodu mümkün mertebede paralel olarak başlatmıştır.

NOT : TPL tarafında geliştiricinin alt-seviye(Low-Level) işlemlerle uğraşmasına gerek yoktur. Ancak bu durum görsel programlama tarafında, Illegal Cross Thread Exception gibi istisnaların olmayacağı anlamına gelmemelidir Undecided

Böylece geldik bir yazımızın daha sonuna. Bu yazımızda TPL alt yapısını en basit ve yalın haliyle tanımaya çalıştık. Elbetteki işimiz bitmedi. Bi dünya işimi var aslında Cool Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

HelloTPL.rar (25,96 kb)

Tags:  
Categories:   TPL
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share