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

TPL – Göz Göre Göre Başımızı Belaya Sokmak

Pazartesi, 21 Haziran 2010 18:08 by bsenyurt

blg209_GirisMerhaba Arkadaşlar,

Bazen göz göre göre başımıza bi ton dert açarız. Kimi zaman başlayacağımız iş bize çok eğlenceli gelebilir (Yandaki resimde yüzü görünmeyen şahsın da bu heyacanla Hamburgere bindiğinden eminiz) Ama işin sonuçlarını biliyorsak eğer, bunu yapmamızın nedeni büyük olasılıkla adrenalindir.

Tabi ki bir yazılımcı için adrenalin genellikle üst yöneticisi tarafından salgılanan bir hormondur. Nitekim yazılımcıların, ilerideki felaketleri kestirerek hareket etmesi ve geliştirmeleri buna göre yapması her zaman kolay olmayabilir. Bir başka deyişle bazı vakalara hazırlıklı olmak için önceden bunları çalışmak gerekmektedir.

İşte bu yazımızda biz de Task Parallel Library için söz konusu olan ve geliştiricilerin başını derde sokacak 2 vaka üzerinde duruyor olacağız. Haydi o zaman parmakları sıvayalım ve işe koyulalım.

Deadlock Durumu

Bu kelime her zaman korkutucu olmuştur. Yazılım Geliştirme serüvenime ilk başladığım yıllarda çoğunlukla veritabanı tarafındaki kilitlenmelerden söz edildiğini çok net hatırlıyorum. Ancak birden fazla iş parçasının da deadlock’ a düşmesi, bir başka deyişle birbirlerini beklemeleri nedeniyle, içinde çalıştıkları Thread’ i(çoğunlukla ana uygulama iş parçası-Main Thread) kitlemeleri söz konusudur. Durumu daha net anlayabilmek için aşağıdaki kod parçasını göz önüne alalım.

using System;
using System.Threading.Tasks;

namespace Disasters
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<double> task1 = null;
            Task<int[]> task2 = null;

            task2 = Task.Factory.StartNew<int[]>(() =>
            {
               double task1Result=task1.Result;
                Random rnd = new Random();
                int[] numbers = new int[5];
                for (int i = 0; i < 5; i++)
                {
                    numbers[i] = rnd.Next(1, 250);
                }
                return numbers;
            }
            );

            task1 = Task.Factory.StartNew<double>(()=>
            {
                double totalValue = 0;
                foreach (int number in task2.Result)
                {
                    totalValue += number;
                }
                return totalValue;
            }
            );

            Task.WaitAll(task1, task2);

            Console.WriteLine("İşlemlerin sonu.Programdan çıkmak için bir tuşa basınız");
            Console.ReadLine();  
        }
    }
}

Aslında kod çok fazla değerlendirilebilir veya anlamlı değildir. Ancak Deadlock oluşumunu görmemiz açısından yeterlidir. Örnekte task1 ve task2 isimli Task nesne örneklerinin, birbirlerinin dönüş değerlerini kullanmaya çalıştığı ifade edilmektedir. İşte Task örneklerinin çalışma zamanında birbirlerini beklemeleri, kendi durumlarının Deadlock olarak set edilmesine neden olacaktır. Bu durum Debug modda aşağıdaki ekran görüntüsünde olduğu gibi görülebilir.

blg209_Debug1

Görüldüğü üzere her iki Task birbirini bekler şekilde kalmıştır. Çok doğal olarak çalışma zamanı çıktısı kapkara bir ekran olacaktır.

blg209_FirstRuntime

Gelelim diğer bir senaryoya.

Döngü Değişkenlerine Dikkat

Bu aslında oldukça eğlenceli ve bir o kadarda beklenmedik sonuçları üreten vakalardandır. Olayı hızlı bir şekilde değerlendirmek adına aşağıdaki kod parçasını göz önüne alabiliriz.

using System;
using System.Threading.Tasks;

namespace Disasters
{
    class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 10; i++)
            {
                Task.Factory.StartNew(() =>
                    {
                        for (int j = 0; j < 125000000; j++)
                        {
                            j++;
                            j--;
                            j*=j;
                        }
                        Console.WriteLine("Güncel Task Id : {0}\tGüncel i : {1}",Task.CurrentId,i.ToString());
                    }
                );
            }

            Console.WriteLine("Kapatmak için bir tuşa basınız");
            Console.ReadLine();
        }
    }
}

Örnek kod ile çalışma zamanında 10 Task örneği başlatılmakta ve bunların lambda ifadeleri(=> Expressions) içerisinden o anki i değerleri ekrana yazdırılmaktadır. Normal şartlarda i değerlerinin her bir Task örneği için farklı olması beklenir. Ancak çalışma zamanına baktığımızda aşağıdaki enteresan sonuçlar ile karşılaştığımızı görebiliriz.

blg209_Runtime2

Görüldüğü üzere bütün Task örnekleri for döngüsü sayacının son değerini ekrana yazdırmaktadır. Çok kolay bir şekilde gözden kaçabilecek bu vaka nedeniyle uzun süre ekrana baka kalabilir ve arkadaşlarımızın bize “Kal Gelmiş” demelerine neden olabiliriz. Oysa ki çözüm son derece basittir.

using System;
using System.Threading.Tasks;

namespace Disasters
{
    class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 10; i++)
            {
                Task.Factory.StartNew((s) =>
                {
                    for (int j = 0; j < 125000000; j++)
                    {
                        j++;
                        j--;
                        j *= j;
                    }
                    int currentI = (int)s;

                    Console.WriteLine("Güncel Task Id : {0}\tGüncel i : {1}", Task.CurrentId, currentI.ToString());
                },i
                );
            }

            Console.WriteLine("Kapatmak için bir tuşa basınız");
            Console.ReadLine();
        }
    }
}

Bu sefer StartNew metodunun farklı bir versiyonu kullanılmıştır. Dikkat edileceği üzere metodun ikinci parametresi olarak i değişkeni kullanılmıştır. Bu aslında State Object olarak düşünülebilir. Dolayısıyla başlatılan her Task örneğine parametre olarak o anki döngü değeri(i değişkeninin değeri) geçirilmektedir. s isimli değişken, for döngüsünden gelen i değişkenini object tipinden temsil ettiği için de, basit bir Cast işlemi yapılması yeterlidir. İşte çalışma zamanı sonuçları.

blg209_Runtime3

Görüldüğü gibi Task örnekleri kendilerine atanan i değerlerini ekrana basmıştır.

Elbette farklı vakalar ve felaket senaryoları da söz konusudur. Bu gibi durumları ilerleyen yazılarımızda ele almaya çalışıyor olacağız. Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Disasters.rar (22,06 kb) [Örnek Visual Studio 2010 Ultimate Sürümünde Geliştirilmiş ve Test Edilmiştir]

Parent-Child Tasks Kavramı

Cuma, 11 Haziran 2010 14:00 by bsenyurt

Merhaba Arkadaşlar,

Planlama gerçek hayatta her zaman karşımıza çıkan ve yaşamımızın, işlerimizin düzenli devam edebilmesi için gereken olmazsa olmazlar arasında yer alan bir kavramdır. Toplantıların planlanmasından tutun da, işlerin hangi sırada yapılacağına karar verilmesine kadar pek çok yerde planlamanın önemini görürüz. Aslında başarılı sistemlerin tasarlanması, çalışması ve istenen sonuçları üretmesi iyi planlamayla ilişkilidir. Tasarımın planlanması, kaynakların planlanması, sistemin önceliklerinin planlanması, müşteri toplantılarının planlanması vs...Bazen kafamızda hayatımızın ilerleyişini planlarız ve bazende yazdığımız kodun fonksiyonelliklerini.

Dolayısıyla planlamanın yazılımın pek çok noktasında da son derece önemli bir rol üstlendiğine şahit oluruz. Aslında planlı olmak, neyin ne zaman nasıl ve ne şekilde yapılacağının bilinmesi, ortaya çıkacak sonuçlarda nasıl hareket edileceğinin tespit edilmesi noktasında son derece hayatidir.

Planlama, bu yazımızda ele alacağımız gibi Task Parallel Library içerisinde de ele alınan bir konudur. Özellikle Parallel.ForEach, Parallel.For, Parallel.Invoke gibi metodlar zaten paralel çalışma için gerekli planlamaları(yapılandırmaları ki neden yapılandırma dediğimi biraz sonra anlayacağız) kendi iç yapılarında gerçekleştiren fonksiyonellikler sunmaktadır.

Ancak Task nesne örneklerinin devreye girdiği noktada, Parent-Child ilişkiler kurularak Planlanmış/Yapılandırılmış görevlerin(Structured Tasks) oluşturulması da mümkündür.

Kişisel Not : Gerçi Structured kelimesini Yapılandırılmış, Denetim Altında Olan anlamlarında da kullanabiliriz ancak burada mevzu bahis olan konu bana göre Task nesne örneklerinin planlı bir şekilde Parent-Child ilişki içerisine alınması ve çalıştırılmasıdır. Tabi ki evdeki hesap her zaman için çarşıya uymaz, uymayabilir. Bu nedenle Parent Task nesne örneklerinin n sayıda Child Task örneğini içeriyor olması da, bir yapının-Structure kurulması olarak düşünülebilir.

İşte bu yazımızda Task nesne örnekleri arasında Parent-Child ilişkiyi incelemeye çalışıyor olacağız. İşe ortam gereklilikleri ile başlamakta yarar olduğu kanısındayım.

Task nesne örnekleri arasında Parent-Child ilişki oluşturulabilmesi için gerekli iki şart vardır. İlk olarak Child olacak Task örneğinin, Parent Task örneğinin çalıştığı yaşam döngüsü(Life Cycle) içerisindeyken oluşturulması(Create) gerekmektedir. İkincil olarak Child Task örneklerinin oluşturulma işlemleri sırasında, TaskCreationOptions enum sabitlerinden AttachedToParent değeri ile üretilmesi gerekmektedir. Bir başka deyişle oluşturulan Task nesne örneğinin, içerisinde çalıştığı Task nesne örneğinin Child' ı olacağının bilinçli bir şekilde bildirilmesi gerekir. Nitekim normal şartlarda varsayılan olarak tüm Task nesne örnekleri Detached pozisyondadır. Çok doğal olarak Parent-Child Task ilişkisi için en az iki Task nesne örneğinin olması gerektiği ortadadır. Wink

Child Task örnekleri, Parent Task örneklerine dönüş durumlarının(Return States) değerlerine göre etkide bulunabilirler. TaskStatus enum sabiti için olası değerler bu noktada önem taşımaktadır. Aşağıdaki şemada olası durumların hangi zaman aralıklarında anlam kazandığı gösterilmeye çalışılmaktadır.

Bir başka deyişe, Child Task nesne örnekleri dönüş değerlerine göre Parent örneklerin yaşam döngülerine(Life Cycle) etkilde bulunurlar diyebiliriz. Peki en basit haliyle Parent-Child Task ilişkisini canlandırmak istersek nasıl bir kod deseni oluşturmamız gerekir? Aşağıda Visual Studio 2010 Ultimate RC ortamında buna istinaden oluşturulmuş bir kod örneği görülmektedir.

using System;
using System.Threading.Tasks;

namespace StructuredTasking
{
    class Program
    {
        static void Main(string[] args)
        {
            Task parentTask = Task.Factory.StartNew(
                () =>
                {
                    Console.WriteLine("Parent Task...");
                    Task childTaskOne = Task.Factory.StartNew(() => { Console.WriteLine("\tChild Task 1"); }, TaskCreationOptions.AttachedToParent);
                    Task childTaskTwo = Task.Factory.StartNew(() => { Console.WriteLine("\tChild Task 2"); }, TaskCreationOptions.AttachedToParent);
                    Task childTaskThree = Task.Factory.StartNew(() => { Console.WriteLine("\tChild Task 3"); }, TaskCreationOptions.AttachedToParent);
                }
            );
            parentTask.Wait();
            Console.WriteLine("İşlemlerin sonu");
        }
    }
}

Kod parçasında dikkat edileceği üzere childTaskOne, childTaskTwo ve childTaskThree isimli Task nesne örnekleri parentTask nesne örneğinin oluşturulduğu metod içerisinde üretilmiş ve başlatışmıştır. Buna göre çalışma zamanında(Runtime) aşağıdakine benzer bir sonuç ile karşılaşılabilir.

Karşılaşılabilir diyoruz, nitekim Parent Task nesne örneğine dahil olan Child Task nesne örnekleri farklı sıralarda çalıştırılabilirler. Örneğimizde Task sırası 1,3,2 şeklindedir ama bu sıra sabit ve kesin değildir. Yani 3,2,1 veya 3,1,2 vb sonuçlar elde edilebilir.(Toplamda 6 farklı sonuç olabileceğini de ifade edebilirizSmile)

Biraz önce Child Task nesne örneklerinin Parent Task örneğine dahil olmaları için AttachedToParent sabit değerini belirtmeleri gerektiğini söylemiştik. Aksi durumda varsayılan olarak Detached olduklarını ifade etmiştik. Yukarıdaki kod parçasında AttachedToParent değerlerini kullanmassak aşağıdaki ekran görüntülerinde yer alanlara benzer sonuçlar ile karşılaşırız.

İlk deneme;

İkinci deneme;

Üçüncü deneme;

Dördüncü deneme;

Görüldüğü gibi bilinçli olarak bir planlama belirtilmediğinden Task örneklerinin yapılandırılmasında sorunlar oluşmuş, bazı çalışmalarda bazı Task örneklerinin sonuçları alınamamıştır.(3 ve 4 teki durumlar)

Parent-Child Task senaryolarında dikkat edilmesi gereken hususlardan birisi de, Parent task nesne örneğinin Final State zaman dilimine girebilmesi için, içerdiği Child Task nesne örneklerinin çalışmalarını tamamlamış olmaları gerektiğidir. Bu durumu analiz etmek için aşağıdaki kod parçasını göz önüne alabiliriz.

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

namespace StructuredTasking
{
    class Program
    {
        static void Main(string[] args)
        {
            Task parentTask = Task.Factory.StartNew(
                () =>
                {
                    Console.WriteLine("Parent Task...");

                    #region IsCompleted

                    Task childTaskOne = Task.Factory.StartNew(() => {
                        Console.WriteLine("\tChild Task 1 Başladı");
                        Thread.Sleep(2000);
                        Console.WriteLine("\tChild Task 1 Bitti");
                    }, TaskCreationOptions.AttachedToParent);
                    Task childTaskTwo = Task.Factory.StartNew(() => {
                        Console.WriteLine("\tChild Task 2 Başladı");                       
                        Thread.Sleep(3000);
                        Console.WriteLine("\tChild Task 2 Bitti");
                    }, TaskCreationOptions.AttachedToParent);
                    Task childTaskThree = Task.Factory.StartNew(() => {
                        Console.WriteLine("\tChild Task 3 Başladı");
                        Thread.Sleep(5000);
                        Console.WriteLine("\tChild Task 3 Bitti");
                    }, TaskCreationOptions.AttachedToParent);

                    #endregion
                }
            );

            while (!parentTask.IsCompleted)
            {
                Console.WriteLine("{0} Parent Task Durumu : {1}",DateTime.Now.ToLongTimeString(),parentTask.Status);
                Thread.Sleep(500);
            }
            Console.WriteLine("{0} Parent Task Durumu : {1}", DateTime.Now.ToLongTimeString(), parentTask.Status);
            Console.WriteLine("İşlemlerin sonu");
        }
    }
}

Kodu çalıştırdığımızda aşağıdakine benzer sonuçlar ile karşılaşırız. Child Task' ler içerisinde bilinçli olarak Thread.Sleep metodu kullanılmış ve uygulamanın belirli süreler boyunca duraksatılması sağlanmıştır. while döngüsünde ise Parent Task nesne örneğinin tamamlanıp tamamlanmadığı sürekli olarak denetlenmektedir.

Dikkat edileceği üzere parentTask nesne örneğinin RanToCompletion moduna, bir başka deyişle Final State' e geçmesi için Child Task nesne örneklerinin tamamının bitmesi beklenmiştir. Üstelik Child Task nesne örnekleri çalışmalarını sürdürürken Parent Task nesne örneğine kendisini beklemesini bildirmektedir ki bu durumda Parent Task ara zaman diliminde durmaktadır ve bu nedenle WaitingForChildrenToComplete modundadır.

Parent Task nesne örneğinin Final State zaman dilimine girmesi anında olası 3 Status değeri bulunmaktadır. Şemamızdan hatırlayacağınız üzere bunlar RanToCompletion, Canceled ve Faulted olarak belirlenmiştir. Child Task örneklerinin başarılı bir şekilde tamamlanmış olmaları, Parent Task örneğinin RanToCompletion değeri üretmesi için yeterlidir. Peki ya Child Task nesne örneklerinden herhangibirinin başlattığı kod içerisinden çalışma zamanına bir Exception fırlatılırsa? Wink Söz gelimi bir önceki örnek kodumuzda yer alan Child Task örneklerinden birisinden bir Exception fırlattığımızı ve bunu yakalamak istediğimizi düşünelim.

Task childTaskOne = Task.Factory.StartNew(() => {
                        Console.WriteLine("\tChild Task 1 Başladı");
                        Thread.Sleep(2000);
                        throw new Exception("Muahahahaha!");
                    }, TaskCreationOptions.AttachedToParent);

...

while (!parentTask.IsCompleted)
            {
                Console.WriteLine("{0} Parent Task Durumu : {1} Exception : {2}", DateTime.Now.ToLongTimeString(), parentTask.Status, parentTask.Exception);
                Thread.Sleep(500);
            }

            Console.WriteLine("{0} Parent Task Durumu : {1} Exception : {2}", DateTime.Now.ToLongTimeString(), parentTask.Status,parentTask.Exception);
            Console.WriteLine("İşlemlerin sonu");

Bu durumda uygulama aşağıdaki örnek çıktıyı verecektir.

Dikkat edilmesi gereken 3 önemli nokta vardır. Parent Task nesne örneği Faulted Status değerini üreterek sonlanmıştır. Diğer yandan ilk Child Task, bir Exception ile sonlandırılsa bile diğer Task' lerin başlattığı işler yürütülmeyi sürdürmüş ve tamamlanmıştır. Bu dikkat edilmesi gereken bir durumdur. Nitekim Child Task' ler ortak bir takım verileri etkiliyor olabilirler ki bu durumda herhangibir istisna, diğer Task' lerin yanlış veriler üzerinden işlem yapmaya devam etmesine neden olabilir. Üçüncü nokta ise Parent Task nesne örneğinin Exception özelliğinin değeridir. Yukarıdaki senaryoya göre bu özellik, Parent Task nesne örneği WaitingForChildrenToComplete durumundayken null değer döndürmektedir. Bir başka deyişle Child Task içerisinden fırlatılan Exception nesnesi aslında bu zaman dilimi içerisinde Parent Task örneğine bildirilmemektedir. Ancak Faulted durumuna düşüldükten sonra söz konusu Exception nesnesi yakalanmaktadır.

Son olarak Parent-Child ilişki ile ilgili olarak şu notu düşebiliriz; Parent Task nesne örnekleri, kaç adet Child Task örneği içerdiğini bilmektedir. Bir başka deyişle kendisine eklenen Child Task' lerin sayısını tutar. Dolayısıyla Child Task nesne örneklerinin, dahil oldukları Parent Task örneğine bir referans bildiriminde bulunduğunu ve hatta tamamlanma durumlarını ilettiklerini ifade edebiliriz. Parent-Child Task' ler arasındaki ilişkiyi fırsat buldukça incelemeye devam ediyor olacağız. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

StructuredTasking_RTM.rar (23,32 kb) [Örnek Visual Studio 2010 Ultimate RTM Sürümü üzerinde geliştirilmiş ve test edilmiştir]

Int32 ve Int64 Haricindekiler için Parallel.ForEach

Pazartesi, 17 Mayıs 2010 09:46 by bsenyurt

Merhaba Arkadaşlar,

Bir kaç yıl öncesine kadar Bizitek firmasına Junior Developer olarak görev almaktaydım. Bu şirkette çalıştığım süre boyunca pek çok projede görev alma fırsatım oldu. Ancak genellikle şirketin iş akışları üzerine geliştirdiği bir ürünün kurulması ve ihtiyaçlara göre düzenlenmesi ile ilgilenmekteydim. Söz konusu uygulamanın belki de en önemli özelliklerinden birisi, kurulduğu firmanın organizasyon ağacını içermesi ve buna göre akış içi adımların kolayca tesis edilebilmesiydi. İşte zaten bazı sıkıntılar da burada başlıyordu. Nitekim bazı firmaların organizasyonel yapıları düzgün değildi. En sık rastlanan vakalardan birisi, herhangibir çalışanın aslında birden fazla görev üstlenmesi nedeniyle organizasyon ağacında birden fazla yerde var olabilmesiydi. Undecided

Dolayısıyla bazı durumlarda en tepeden aşağıya doğru inen ve bağlı liste(Linked List) benzeri bir oluşumun sağlanması zorlaşmaktaydı. Her neyse...Eminim bu sorunlar çoktan aşılmıştır. Ancak bir önceki cümlede yer alan bağlı liste tarzı yapıların başında dolaşan bir kara bulut daha mevcuttur. Sorunun kaynağında paralel programlama amacıyla .Net ortamına kazandırılan Parallel.ForEach döngüsü yer almaktadır. Dilerseniz öncelikle sorunu masaya yatıralım. Bu amaçla aşağıdaki kod içeriğine sahip Employee isimli basit bir sınıfımız olduğunu düşünelim.

class Employee
    {
        public string Profession { get; set; }
        public string Name { get; set; }
        public Employee Parent { get; set; }
        public Employee Child { get; set; }       
    }

Employee sınıfı ile bir şirketin belirli organizasyonel pozisyonlarını ifade etmek istediğimizi düşünebiliriz. İçeriğinde yer alan Parent ve Child isimli özellikler dikkat edileceği üzere Employee tipindendir. Buna göre bir Employee nesne örneğinin altına ve üstüne başka bir Employee referansının atanması mümkündür. Dolayısıyla bir ağaç yapısının kolayca oluşturulması mümkündür. Elbetteki sembolik olarak. Söz gelimi;

Director
--->Project Manager
------>Technical Project Manager
--------->Senior Developer
------------>Junior Developer

gibi.

Yukarıdaki gibi bir yapıyı oluşturduğumuzda Parent ve Child özellikleri sayesinde organizasyon içerisinde aşağı ve yukarı doğru kolayca hareket edebileceğimizi görebiliriz. Bu durum aşağıdaki Console uygulamasında ele alınmaktadır.

using System;

namespace ParallelForNonIntegralRanges
{
    class Program
    {
        static void Main(string[] args)
        {
            Employee root = Fill();
                       
            #region Case 1 - Seri for döngüsü

            for (Employee emp = root; emp !=null ; emp=emp.Child)
            {
                Console.WriteLine("{0} {1}",emp.Profession,emp.Name);
            }

            #endregion
        }

        static Employee Fill()
        {
            Employee d = new Employee { Profession = "Director",Name="Bill" };
            Employee pm = new Employee { Profession = "Project Manager",Name="Steve" };
            Employee tpm = new Employee { Profession = "Technical Project Manager",Name="Joe" };
            Employee sd = new Employee { Profession = "Senior Developer",Name="Nicole" };
            Employee jd = new Employee { Profession = "Junior Developer",Name="Burak" };

            d.Parent = null;
            d.Child = pm;

            pm.Parent = d;
            pm.Child = tpm;

            tpm.Parent = pm;
            tpm.Child = sd;

            sd.Parent = tpm;
            sd.Child = jd;

            jd.Parent = sd;
            jd.Child = null;

            return d;
        }
    }

    class Employee
    {
        public string Profession { get; set; }
        public string Name { get; set; }
        public Employee Parent { get; set; }
        public Employee Child { get; set; }       
    }
}

Dikkat edileceği üzere for döngüsünden yararlanılarak Director' den en alt kademede yer alan Junior Developer' a kadar ilerlenilmesi sağlanılmaktadır. İşte bu örnek kod parçasının çalışma zamanı çıktısı;

Peki ya Employee gibi bir tipin çalışma zamanındaki örneği ve içeriğindeki bağlı referanslar arasında Parallel.For döngüsü ile ilerlenilmek istenirse? Wink Nitekim elimizin alında binlerce ve hatta daha fazla elemandan oluşan bir ağaç yapısı olabilir ve bu yapı üzerindeki elemanlarda bazı işlemlerin yapılması istenebilir. Bu durumda işlemlerin daha hızlı gerçekleştirilebilmesi için paralel programlama yetenekleri göz önüne alınabilir. Ancak ortada önemli bir sorun vardır. Parallel.For metodunun versiyonlarına bakıldığında int(Int32) ve long(Int64) tipleri ile çalıştığı görülecektir. Bu durumda Employee nesne örnekleri için Parallel.For döngüsünü kullanmamız mümkün değildir. O zaman belki Parallel.ForEach döngüsü tercih edilebilir. Edilebilir mi acaba? Bunu denediğimizde derleme zamanında aşağıdaki sonuçlar ile karşılaşmamız kaçınılmazdır.

Bu son derece doğaldır. Nitekim Employee tipinin IEnumerable<T> gibi bir arayüz implemantasyonu yapmadığı ortadadır. Kaldı ki foreach döngüleri üzerinde hareket edecekleri tipler için bu arayüz implemantasyonunun yapılmasını beklemektedir. O halde farklı bir yardımcı metoddan yararlanalılabilir. Aşağıdaki gibi;

static IEnumerable<Employee> Iterate(Employee root)
{
   for (Employee emp = root; emp != null; emp = emp.Child)  
   {
      yield return emp;  
   }
}

Yaşananlardan : Çok eskiden C#Nedir? adına düzenlenen bir etkinlikte C# 2.0 ile birlikte gelen yenilikleri anlatmaktaydım. C# 2.0 ile birlikte gelen önemli yeniliklerden birisi de yield anahtar kelimesi ile bilinen iterasyon deseninin çok daha kolay uygulanabilmesiydi.Hatta bu konu ile ilişkili olaraktan amatör olduğum dönemlere ait bir yazım dahi bulunmaktadır. C# 2.0 İçin İterasyon Yenilikleri(5.7.2005) Şimdi o yeniliğin buradaki örnekte işimizi ne kadar kolaylaştırdığını bir kere daha görebiliyoruz. Hey gidi günler. Laughing

Iterate isimli metod yield return kullanarak IEnumerable<Employee> tipinden bir sonuç kümesi döndürmektedir ve bu aslında Parallel.ForEach döngüsünün tamda istediği referanstır. Dolayısıyla artık  aşağıdaki gibi bir kod parçası derlenip çalıştırılabilir.

Parallel.ForEach<Employee>(
   Iterate(root),
   emp => Console.WriteLine("{0} {1}", emp.Profession, emp.Name)
);

Dikkat edileceği üzere ilk parametre ile ForEach döngüsünün beklediği IEnumerable<Employee> içeriği verilmektedir. Bu durumda kod sorunsuz bir şekilde çalışacaktır. Tabiki ekrana yazdırılma sırası değişmeyecektir. Nitekim buradaki tip içeriği saniyenin çok küçük bir diliminde tamamlanacağından farklı Thread' lerin işi ele almasına zaman kalmayacaktır. Sakın sırası karışık bir organizasyon ağacı yazdırılmasını beklemeyin. Yaptığımız sadece ve sadece Int32/Int64 dışında kalan ve IEnumerable<T> gibi bir arayüzü implemente etmeyen bir tipe ait çalışma zamanı nesne örneği ve bağlı referanslarının, Parallel.ForEach döngüsü tarafından dolaşılabilmesini sağlamaktır.

Ancak yine de dikkat edilmesi gereken önemli bir durum vardır. IEnumerable<T> thread safe olmadığından döngünün kullandığı veriye olan erişimler sırasında kilitleme tekniklerinden yararlanılması gerekebilir ki bu da aslında performansı olumsuz yönde etkileyebilecek bir durumdur. Burada performansı arttırmaya yönelik olarak belkide Iterate metodunun sonucunun ToArray gibi bir metod yardımıyla diziye çevrilmesi düşünülebilir. Aşağıdaki kod parçasında görüldüğü gibi.

Parallel.ForEach<Employee>(
   Iterate(root).ToArray(),
   emp => Console.WriteLine("{0} {1}", emp.Profession, emp.Name)
);

Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

ParallelForNonIntegralRanges_RTM.rar (27,05 kb) [Örnek uygulama Visual Studio 2010 Ultimate RTM sürümü üzerinde geliştirilmiş ve test edilmiştir]

Paralel Programlamada Performans, Hız, Verimlilik ve Ölçeklenebilirlik Ölçümleri

Pazartesi, 22 Şubat 2010 10:05 by bsenyurt

Merhaba Arkadaşlar,

Ben Matematik Mühendisliği eğitimi almış bir bireyim. Öğrenim hayatım boyunca en çok yaptığım işlerden birisi, matematiksel teoremlerin bilimsel ispatlarını gerçekleştirmek olmuştur. Hemen hemen mühandisliğin her alanındaki farklı problemlerin modellenmesi ve ispatlarının yapılarak en uygun yol olduklarının gösterilmesi adına pek çok kağıt karalamış ve tüketmişimdir. Zaman zaman neden yaptığımızı anlamadığım ispatlardan tutunda, lastiğine konumuş olan sineğin bisikletin ileriye yönlü ama düz bir rotada olmayan hareketi boyunca çizdiği sarmalımsının denklemini çıkartmaya kadar matematiğin bir o kadar garip ama gizemli evreninde dolaşıp durduğumu hatırlıyorum. Hatta bir gün Matematik Analiz dersinin finalinde karşılaştığım bir soruda ne hikmetse 1=2 sonucuna ulaşmışımdır. Halbuki 1=1' e ulaşmış olmam gerekirdi. Sealed  

Tabi zaman ilerleyip iş para kazanmaya gelince kimsenin teorem ispatları ile uğraşmadığnı acı olarak farketmiştim. İlgi duyduğum yazılım sektörüne gireli uzun yıllar olduğu için matematiksel teorem ispatlarında tam anlamıyla pas tutmuş durumdayım. Yine de zaman zaman yazılım içerisinde matematiği basit haliyle bile görebilmek, en azından bazı konuların ispatında kullanabiliyor olmak sevindirici.

Gelelim bu yazımızın konusuna. Bildiğiniz üzere bir süredir paralel programlama ile ilişkili konuları incelemeye çalışıyor ve öğrenebildiklerimi sizlerle paylaşıyorum. Yine bu vesile ile geçtiğimiz haftalar içerisinde internette dolaşırken gözüme ilişen kısa ve özlü bir yazı ile karşılaştım. Microsoft Paralel programlama takımı tarafından yayınlanan FAQ girdisinde, çeşitli kriterlere göre hangi kodun daha iyi olduğunun ölçümlenebilmesi için hangi kriterlere bakılabileceği özetlenmektedir. Kısaca elimizde aşağıdaki tabloda yer alan kriterler mevcut. Bu kriterlere göre seri ve paralel olarak yazılmış kod algoritmalarının kıyaslanması mümkün. Özellikle yazdığımız paralel program kodlarının normal versiyonlarına göre daha iyi olup olmadıklarının tespit edilmesi noktasında son derece mühim kriterler olduklarını düşünmekteyim.

Kriter

Açıklama

Performance(Performans)

Genel olarak seri ve paralel yazılmış kodların performans ölçümlerinde algoritmanın toplam icra süreleri hesaba katılmaktadır. Çoğunlukla ve pek tabii olarak bir algoritmanın yürütülme süresinin diğerine göre daha düşük olması tercih edilir ki bu iyi bir perfomans anlamına gelmektedir.

SpeedUp(Hızlanma)

Hızlanma değerini hesap etmek için şu formül kullanılır;

SpeedUp = Seri Çalışma Süresi / Paralel Çalışma Süresi

Bu formülün sonucuna göre bir algoritmanın diğerinden kaç kat hızlı olduğu belirlenebilir.

Efficiency(Verimlilik/Etkinlik)

Tabiki yazılan algoritmanın çalıştırıldığı seri veya paralel işleyişin hangisinin tercih edileceği kararını vermede rol oynan kriterlerden birisi de hangisinin daha verimli olduğudur. Verimliliği veya etkinliği hesap etmek içinse aşağıdaki formülden yararlanıldığı görülmektedir.

Efficiency = Hızlanma / İşlemci Çekirdek Sayısı

Scalability(Ölçeklenebilirlik)

Ölçeklenme bilimsel alanda son derece yaygın kullanılan etkili bir terimdir. Şu anki konumuza baktığımızda ise yazılan algoritmanın seri ve paralel denemelerine ait SpeedUp değerlerinin farklı sayıda çekirdek/işlemci için nasıl sonuçlar verdiği ile alakalıdır. İşlemci veya çekirdek sayısının artması ile SpeedUp değerlerinin düşmesi bir başka deyişle daha hızlı sonuçlar elde edilmesi, pozitif ölçeklenmenin ispatı olarak düşünülebilir. Yani işlemci/çekirdek sayısının artması dolayısıyla ortamın büyümesi karşılığında hızlanmanında artması beklenir.

Kişisel Not : Her ne kadar söz konusu yazıda sadece çekirdek sayıları hesaba katılsa da bana göre ram ve işlemci tiplerinin de söz konusu algoritmanın seri veya paralel çalışması durumlarındaki ölçeklenmeyi etkileyeceği ve hesaba katılması gerektiği düşüncesindeyim.

Haydi gelin buradaki ölçüm değerlerini örnek bir kod üzerinden incelemeye çalışalım. Bu amaçla Visual Studio 2010 Ultimate RC ve .Net Framework 4.0 RC üzerinde basit bir Console uygulaması geliştiriyor olacağız. Örneğe ait senaryomuz ise şu şekilde olacaktır.

Bir klasör içerisinde yer alan jpg uzantılı resim dosyalarının boyutlarının arttırılmasını ele alan bir kod parçası geliştireceğiz. Resimlerin boyut arttırım işleminin zaman alan ve yorucu bir işlem olduğu düşünüldüğünde, seri ve paralel kodların ürettiği sonuçları yukarıdaki kriterler eşliğinde değerlendirmeye çalışacağız. Ne yazık ki elimde sadece çift çekirdekli iki makine olduğundan Scalability kriterini bu örnekte sizlere gösteremiyorum Undecided Ancak sizler uygun test ortamlarına sahipseniz, ölçeklenebilirlik kriterini değerlendirebilirsiniz ki değerlendirmenizi tavsiye ederim. İşte Console uygulaması kodlarımız...

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

namespace ProofOfConcept
{
    class Program
    {
        static void Main(string[] args)
        {
            string[] files = Directory.GetFiles("..\\..\\Images\\", "*.jpg");

            // Execution süreleri tüm kriterlerde önemlidir. Bu hesaplama için Diagnostics isim alanında yer alan Stopwatch tipinden yararlanılır
            Stopwatch watcher = new Stopwatch();
            watcher.Start();
            SerialExecution(files);
            watcher.Stop();
            // Seri çalışmanın toplam işlem süresi milisaniye cinsinden elde edilir
            float serialElapsed=Convert.ToSingle(watcher.ElapsedMilliseconds);
            
            watcher.Restart();
            ParallelExecution(files);
            watcher.Stop();
            // Paralel çalışmanın toplam işlem süresi milisaniye cinsinden elde edilir
            float parallelElapsed = Convert.ToSingle(watcher.ElapsedMilliseconds);

            // Kriterler hesaplanır
            Console.WriteLine("Serial Performance {0} mili saniye \t Parallel Perfomance {1} mili saniye",serialElapsed,parallelElapsed);
            float SpeedUp = serialElapsed / parallelElapsed;
            Console.WriteLine("SpeedUp {0}",SpeedUp);
            double Efficiency = SpeedUp / Environment.ProcessorCount;
            Console.WriteLine("Efficiency {0} (% {1})", Efficiency,Efficiency*100);
            Console.WriteLine("Scalability bu örneğimizde ne yazıkki test edilemedi");
        }

        // Seri çalıştırma metodumuz
        static void SerialExecution(string[] fileList)
        {
            foreach (string file in fileList)
            {
                Resize(file);
            }
        }

        // Paralel çalıştırma metodumuz
        static void ParallelExecution(string[] fileList)
        {
            // Dosyaları Paralel ForEach döngüsüne göre ele almakta.
            Parallel.ForEach<string>(fileList, s => Resize(s));
        }

        // Resim dosyasını yeniden boyutlandırmak için kullandığımız metod
        static void Resize(string fileName)
        {
            // Önce Image tipi elde edilir
            Image img = Image.FromFile(fileName);
            //Yeni genişlik ve yükseklik değerleri belirlenir. Örnek olarak % 40 artım yapılmıştır
            int newWidth = Convert.ToInt32(img.Width * 1.40);
            int newHeight = Convert.ToInt32(img.Height * 1.40);
            // Yeni boyutlarına göre bir Bitmap nesnesi örneklenir
            Bitmap btmp = new Bitmap(img, new Size(newWidth, newHeight));
        }        
    }
}

Uygulamayı kendi makinemde (Intel Core2Duo, 2.5 Gb Ram), 45 resim(44.8 Mb) üzerinde test ettiğimde aşağıdaki sonuçları elde ettim. Burada seri ve paralel yürütmeler arasındaki farklılıklar kriterler bazında açık bir şekilde ortaya çıkmakta.

Buna göre paralel çalıştırmanın performansı seri olana göre daha yüksektir. Ayrıca paralel çalıştırma, seri olana göre 1,55 kata kadar daha hızlıdır. İlaveten paralel olan çalıştırmanın seri olana göre %77 daha verimli/etkili olduğu sonucuna ulaşılabilir. Tabi bu kodu farklı sayıdaki işlemci veya çekirdek sayısına sahip sistemlerde defalarca test edip bir istatistik çıkartmak ve bunun sonuçlarına bakmak daha doğru olacaktır. İsterseniz bu kod parçasında farklı bir deneyimi tecrübe edebilirsiniz. Örneğin SerialExecution ve ParallelExecution metodlarını arka arkaya 10 kez çalıştırıp sonuçları Excel tablosunda istatistikleştirip gerekli analizleri yapabilir ve hangisini tercih edebileceğinize daha kolay karar verebilirsiniz. En azından ispatı daha güçlü kılarsınız. Wink Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

ProofOfConcept_RC.rar (4,33 mb) [Örnek Visual Studio 2010 Ultimate RC sürümü üzerinde geliştirilmiş ve test edilmiştir]

Webiner - .Net 4.0 ile Paralel Programlamaya Giriş [Beta 2]

Cumartesi, 26 Aralık 2009 01:45 by bsenyurt

Merhaba Arkadaşlar,

Bundan yıllar önce üniversitede öğrenciyken ilk kişisel bilgisayarımı almak üzere rahmetli babam ile birlikte Perpa' ya gittiğimi hatırlıyorum. O sıralar piyasada var olan bilgisayar dergilerini hatırlıyorum da...486 DX-33 tabanlı işlemcilere sahip sistemler anlatılıyor ve öneriliyordu. Ancak bütçe belli olunca Perpa' daki mağazada ancak 486 SX-25 işlemcili bir modeli alabileceğimizi farketmiştik. Aradaki tek farkın, SX-25' in DX-33'e göre matematiksel hesaplamalarda daha yavaş olması olduğunu hatırlıyorum. Tabi bu eksiklik, matematiksel hesaplamalarda, dosya giriş çıkış işlemlerinde, sunucu bazlı süreçlerde, yapay zeka algoritmalarında, grafik işleme uygulamalarında dez avantaj oluşturacak bir fark yaratıyordu. Özellikle hız ve performans açısından. Ancak o zamanlar daha çok oyunlara olan etkisini düşünüyordum. Wink 

Gel zaman git zaman Intel firmasının kurucusu Gordon Moore' un meşhur yasasını okudum. 18 ayda bir ikiye katlanan ve üretim maaliyetleri aynı kalan yada azalan transistor sayılarından bahsediyor ve hızların nasıl artacağını ifade ediyordu. (Tabi bu kanun günümüzde geçerliliğine neredeyse yitirdi.) Sonrasında ise işin ardı arkası kesilmedi. 200 MMX, Pentium, Celeron işlemciler derken bir baktık diz üstü bilgisayarlarımızda bile 4 çekirdek var. (Örneğin şu an bu yazıyı yazdığım bilgisayarda çift çekirdek işlemci bulunmakta) Artık 32 işlemcili bir bilgisayarın maaliyeti bundan onlarca yıl öncesi kadar yüksek değil. Hatta bu kadar çok işlemciye sahip sistemleri kurmak artık hayal değil. Hal böyle olunca, uygulamalarda Multi-Thread yaklaşımlarda bir hayli önem kazandı. Özellikle son yıllarda Microsoft bu anlamda yazılımcıların daha kolay kod geliştirebilmesine olanak tanıyan yenilikler duyurdu. İşte bunlardan biriside paralel programlama alt yapısı. Bu Webinerimizde .Net Framework 4.0 içerisine gömülü olarak gelen paralel programlama alt yapısını inceldik. Kaçıran arkadaşlarımız NedirTV?com adresinden söz konusu webineri indirip izleyebilirler. İyi seyirler dilerim.

Süre : 58:55

Parallel.For Metodu için Stop, Break Kullanımı [Beta 1]

Perşembe, 18 Haziran 2009 18:32 by bsenyurt

Merhaba Arkadaşlar,

Parallel.For metodu bildiğiniz gibi döngüsel işlemleri birden fazla göreve bölerek kısa sürede yapılmasına olanak sağlamaktadır. Bu yazımda, kelimeler ile ifade etmeyi bir türlü beceremediğim ancak bir örnek üzerinden sizlere aktarabileceğim Stop ve Break metodları üzerinde durmaya çalışacağım. Aslında amaç tahmin edeceğiniz üzere paralel çalışan döngü içerisinden çıkmak. Bu ardışıl çalışan bir for döngüsü göz önüne alındığında problem değil. Yada önemsenmesi gereken sorunlara yol açabilecek bir konu değil. Nitekim tek bir Thread söz konusu. Ancak Parallel.For metodu işlemleri gerçekleştirirken birden fazla Task' in başlatılmasına neden olmaktadır. Bu durumdada Stop veya Break gibi iki farklı metodun nasıl davranış göstereceğini bilmekte yarar vardır. İşte konuyu anlayabilmek için Visual Studio 2010 Beta 1 sürümünde geliştirdiğim örnek Console uygulaması kodları.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Concurrent;
using System.Threading;

namespace ParallelForStopBreak
{
    class Program
    {
        static void Main(string[] args)
        {
            ConcurrentDictionary<int, DateTime> values = new ConcurrentDictionary<int, DateTime>();

            // ls ParallelLoopState tipinden olup derleyici tarafından üretilmektedir.
            Random rnd = new Random();

            Parallel.For(0, 1000, (i,ls) =>
                {
                    // Güncel ThreadId değerini alalım.
                    string threadId=Thread.CurrentThread.ManagedThreadId.ToString();

                    values.TryAdd(i, DateTime.Now);
                    Thread.Sleep(500);
                    Console.Write("({0}) {1} ",threadId,i.ToString());

                    #region Stop Durumu

                    if (rnd.Next(1, 100) == 3) // Eğer rastgele üretilen sayı 3 ise Stop metodu çağırılır.
                    {
                        ls.Stop();
                        Console.WriteLine("\t\n {0} için Stop çağrısı yapıldı", threadId);
                    }
                    if (ls.IsStopped) // Eğer çalışan paralel Thread durdurulmuşsa
                        Console.WriteLine("\n{0} durduruldu", threadId);

                    #endregion
                }
            );

            Console.WriteLine("{0} eleman eklendi.\nÇıkmak için bir tuşa basınız.",values.Count.ToString());
            Console.ReadLine();
        }
    }
}

Program kodumuzda 0' dan 1000'e kadar zaman değerlerinin üretilip bir ConcurrentDictionary<int,DateTime> koleksiyonuna eklenmesi söz konusudur. Döngü içerisinde Random sınıfından yararlanılarak 3 değeri kontrol edilmektedir. Eğer 3 değerine denk gelinirse Stop metodu çağırılır. Program çalışması sonucu her seferinde farklı sonuçlar üretilmesi olasıdır. Bunu peşinen söyliyim. Nitekim her defasında farklı sayıda ve sırada görevler çalışmaktadır. Örnek sonuçlardan birisi aşağıdaki ekran görüntüsünde olduğu gibidir.

Evettt. Şimdi bu çalışma şeklini bir değerlendirelim. Parallel.For metodu, 12,6,10,11,21,20,19,18,13,16,17,15,14 numaralı Thread' leri oluşturmuştur. Çalışma sırasında, 12 nolu Thread görevini yürütürkende Stop çağrısı gelmiştir. Bu durumda çalışmakta olan tüm paralel görevlere durdurulma emri gitmektedir. Ancak 12 nolu Thread sırasında Stop emri gelmesine rağmen diğer Thread' ler kısa bir sürede olsa(örneğe göre birer eleman ekleme süresi kadar) geç durmuştur. Thread' lerin durup durmadıkları, dikkat edeceğini üzere ParallelLoopState referansının IsStopped özelliği ile anlaşılmaktadır.

Peki ya Break metodu nasıl bir etkide bulunmaktadır. Bu amaçla Parallel.For metodu içerisine aşağıdaki kodları ekledim.

if (rnd.Next(1, 100) == 3) // Eğer rastgele üretilen sayı 3 ise Break metodu çağırılır.
{
 ls.Break();
 Console.WriteLine("\t\n {0} için Break çağrısı yapıldı.", threadId);
}

Aslında bu kez Stop metodu yerine sadece Break metodunu kullandığımızı görebiliriz. Peki ya çalışma zamanı? Her zamanki her çalışma sonrası farklı sonuçların üretildiği ortadadır. Aşağıdaki ekran görüntüsünde bu çalışmalardan birisi ele alınmaktadır.

Durumu değerlendirmeye çalışalım. Herşeyden önce birden fazla Thread' in çalıştığı kolayca gözlemlenebilir. Örnekte 10, 11, 6, 13, 12, 14, 15, 19, 21, 17 numaralı Thread' ler çalıştırılmaktadır. Derken çalışma zamanının bir anından, 14ncü Thread için Break çağrısı gelmiştir. Bunun üzerine 14 numaralı Thread durdurulmuştur. Diğer yandan, Break metodu ile karşılaşıncaya kadar başlatılan diğer Thread' ler çalışmalarına devam etmektedir. İşte Stop metodu ile aradaki önemli bir farklılık. Yinede ilerleyen kısımlarda diğer Thread' lerden bazılarının yürütülmesi esnasında Break çağrısı ile karşılaşılması olasıdır ki 13ncü Thread için bu gerçekleşmiştir. Tabiki bu çağrı sonrasında 13ncü Thread' de sonlandırılmış ama daha önceden başlatılmış diğer Thread' ler kendilerine ayrılan üst sınır değerine kadar yürümeye devam etmiştir. Nitekim ilerleyen kısımlarda diğer Thread' ler için Break komutu ile karşılaşılmamıştır.

Sanıyorumki Stop ve Break metodları arasındaki farkı biraz biraz kendini göstermeye başladı. 

Yinede şu ana kadar yaptığım analizde havada kalan noktalar var gibi hissediyorum. Undecided Farklılığı tam olarak göremediğimi itirifat etemliyim. Bu nedenle Break tekniği ile ilişkili kod parçasında if kontrolünü aşağıdaki gibi değiştirdim ve Thread.Sleep süresini biraz daha kısalttım. Amaç çalışan Thread' lerden 10 numaralı Id' ye sahip olana denk gelindiğinde Break komutu kullanmak ve diğer Thread' lere ne olacağını anlamaktı.

if(threadId=="10")

Volaaaa... Laughing Bu durumda oluşan farklı çalışma zamanı sonuçlarından birisi aşağıdaki ekran görüntüsündeki gibi oldu.

Ve diğer bir denemenin sonucu;

Bu sonuçlara ve diğerlerine baktığımda 1000 adımlık iterasyonun, Thread' lere farklı sayılarda bölündüğünü farkettim. Diğer yandan 10 numaralı Thread çalışmaya başlayıp bir eleman eklendikten sonra gelen Break metodu çağrısı nedeniyle durdurulmuştu. Diğer Thread' ler ise çalışmalarına devam ederek kendilerine ayrılan limitler dahilinde eleman eklemeyi sürdürmüşlerdi. O zaman aynı vakada Stop metodu ne yapar diye insan ister istemez merak ediyor. Bunun üzerine Stop metodunun kullanıldığı senaryodaki if koşulunda 10 numaralı Thread' i kontrol etmeye karar verdim. Ve işte çalışma zamanı sonuçlarından birisi;

Görüldüğü gibi 10ncu Thread çalışmaya başlayıp 1 eleman ekledikten sonra gelen Stop metodu nedeniyle hem kendisi hemde diğer Thread' ler mümkün olan en kısa sürede durdurulmuştur.

Sanıyorumki artık Stop ve Break arasındaki farkı daha iyi görebiliyoruz. Laughing Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

ParallelForStopBreak.rar (21,27 kb)

Concurrent Collections : Macera BlockingCollection ile Devam Ediyor [Beta 1]

Salı, 16 Haziran 2009 18:54 by bsenyurt

Merhaba Arkadaşlar,

Bir önceki blog yazımda paralel programlama kabiliyetlerinden birisi olan Concurrent Collections(Eş Zamanlı Koleksiyonlar) kavramını incelemeye çalışmıştım. Ne varki kendimi bunlara olan gereklilikler konusunda bir süredir ikna edebilmiş değilim. Dolayısıyla ihtiyaçları ortaya koymak adına basit bir senaryo üzerinden ilerlemeye karar verdim. Aslında eş zamanlı koleksiyonların kullanılması için en büyük gereksinim, bir koleksiyonun elemanları üzerinde aynı anda işlemler yapılmak istenmesi halinde ortaya çıkmaktadır. Konuyu daha net kavrayabilmek adına şöyle bir senaryoyu geliştirmeye karar verdim; Bir metin dosyasında | işaretleri ile birbirlerinden ayrılmış text tabanlı verilerin, generic bir List koleksiyonu içerisine alınması ve sonrasında ise bu koleksiyon elemanlarının içeriklerinin değiştirilmesi. Tabiki burada iki ana iş var. Metin dosyasının ayrıştırılıp(parse) koleksiyon içerisinde toplanması ilk adım olarak düşünülebilir. İkinci adımda ise, bu koleksiyon üzerinde ileri yönlü bir iterasyon ile o anki nesne örneği üzerinde değişiklik yapılmaya çalışılması(örneğin maaş bilgisinin değiştirilmesi) durumu ele alınmalıdır. Ancak burada küçük ama önemli bir maddemiz var; bu iki adımdaki işlemleri paralel olarak gerçekleştirebilmekWink Dolayısıyla iki farklı Thread' in birlikte çalışarak söz konusu işlemleri yapması sağlanabilir. Bu fikirden yola çıkarak aşağıdaki bir Console uygulamasını geliştirdim. Projede yer alan ana sınıflar aşağıdaki class diagram çizelgesinde görüldüğü gibidir.

Örnekte text tabanlı içeriği tutan Personel.txt dosyasının içeriğini ise aşağıdaki gibi tamamen atmason verilerden oluşturmuş bulunmaktayım.

 

Program kodları ise;

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace ConcurrentCollections2
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                PersonManager manager = new PersonManager();
                manager.StartTest();
            }
            catch (Exception excp)
            {
                Console.WriteLine(excp.Message);
            }

            Console.ReadLine();
        }       
    }

    // Metin dosyasındaki bilgilerin nesne karşılıkları için tasarlanmış Person sınıfı
    class Person
    {
        public int PersonId { get; set; }
        public string Name { get; set; }
        public string Title { get; set; }
        public decimal Salary { get; set; }
    }

    // Test metodunu içeren Test sınıfımız
    class PersonManager
    {
        // Person bilgilerinin tutulacağı generic List koleksiyonu
        List<Person> personList = new List<Person>();

        public void StartTest()
        {           
            // GetPersonList metodu için bir Thread tanımlanır
            Thread trd1 = new Thread(new ThreadStart(GetPersonList));
            // ProcessPersonList metodu için bir Thread tanımlanır
            Thread trd2 = new Thread(new ThreadStart(ProcessPersonList));

            // Thread' ler başlatılır
            trd1.Start();
            trd2.Start();
        }

        // Metin dosyasından okuma işlemini yaparak personList isimli generic List koleksiyonuna Person nesne örneklerinin eklenmesi işlemini üstlenir
        private void GetPersonList()
        {
            // Personel.txt dosyasındaki tüm satırlar string[] dizisine alınır
            string[] persons = File.ReadAllLines(System.Environment.CurrentDirectory + "\\Personel.txt");

            // Her bir satır ele alınır
            foreach (string person in persons)
            {
                // Satır | işaretine göre ayrıştırılır
                string[] values = person.Split('|');

                // Ayrıştırma sonucu elde edilen değerlere göre Person nesne örneği oluşturulur
                Person prs = new Person
                {
                    PersonId = Convert.ToInt32(values[0]),
                    Name = values[1],
                    Title = values[2],
                    Salary = Convert.ToDecimal(values[3])
                };
                // Persone nesne örneği koleksiyona eklenir
                personList.Add(prs);
                // Console penceresinden bilgilendirme yapılır
                Console.WriteLine("{0} listeye eklendi", prs.Name);

                Thread.Sleep(250); // işleyişi kolay takip edebilmek için küçük bir zaman aldatmacası
            }
        }

        // personList isimli generic List koleksiyonundaki her bir Person nesne örneğinin Salary bilgisini değiştirir
        private void ProcessPersonList()
        {
            // Koleksiyondaki her bir Persone nesne örneği ele alınır
            foreach (Person person in personList)
            {
                // O anki Person nesne örneğinin Salary özelliğinin değeri değiştirilir
                person.Salary += 1.18M;

                // Console ekranında bilgilendirme yapılır
                Console.WriteLine("\t {0} için maaş {1} olarak değiştirildi", person.Name, person.Salary);
                Thread.Sleep(250); // işleyişi kolay takip edebilmek için küçük bir zaman aldatmacası
            }
        }
    }
}

PersonManager sınıfı içerisinde yer alan StartTest metodu kendi içerisinde iki farklı Thread oluşturmakta ve çalıştırmaktadır. Bu Thread' lerden birisi GetPersonList fonksiyonunu kullanarak koleksiyona veri ekleme işlemini üstlenmektedir. İkinci Thread tarafından çağırılan ProcessPersonList metod ise, maaş bilgilerini düzenlemektedir. Kritik olan nokta her iki Thread' in aynı koleksiyon nesne örneği üzerindeki elemanları kullanmak istemesidir. Programı çalıştırdığımda aşağıdaki sonuç ile karşılaştım;

Görüldüğü gibi koleksiyon zaten farklı bir Thread içerisinde ele alındığından, düzenleme işlemi yapılmasına izin verilmemektedir. İşte eş zamanlı koleksiyonları ele almak için geçerli bir neden. Peki ama hangi eş zamanlı koleksiyon Undecided Bu noktada bir önceki blog yazımın sonunda verdiğim sözü hatırlıyorum. BlockingCollection<T> koleksiyonu. Bunun üzerine kodu aşağıdaki şekilde değiştirdim.

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace ConcurrentCollections2
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                PersonManager manager = new PersonManager();
                manager.StartTestConcurrent();
            }
            catch (Exception excp)
            {
                Console.WriteLine(excp.Message);
            }
        }       
    }

    // Metin dosyasındaki bilgilerin nesne karşılıkları için tasarlanmış Person sınıfı
    class Person
    {
        public int PersonId { get; set; }
        public string Name { get; set; }
        public string Title { get; set; }
        public decimal Salary { get; set; }
    }

    // Test metodunu içeren Test sınıfımız
    class PersonManager
    {
        // Person bilgilerinin tutulacağı generic List koleksiyonu
        // List<Person> personList = new List<Person>();
        BlockingCollection<Person> personList = new BlockingCollection<Person>();

        public void StartTestConcurrent()
        {
            // Task' leri başlatalım
            Task[] tasks ={ Task.Factory.StartNew(() => { GetPersonList(); }),
                              Task.Factory.StartNew(() => { ProcessPersonList(); })
                          };

            // Tüm Task' ler tamamlanıncaya kadar bekle
            Task.WaitAll(tasks);
           
            Console.WriteLine("İşlemler sona erdi. Programdan çıkmak için bir tuşa basın");
            Console.ReadLine();
        }

        // Metin dosyasından okuma işlemini yaparak personList isimli generic List koleksiyonuna Person nesne örneklerinin eklenmesi işlemini üstlenir
        private void GetPersonList()
        {
            // Personel.txt dosyasındaki tüm satırlar string[] dizisine alınır
            string[] persons = File.ReadAllLines(System.Environment.CurrentDirectory + "\\Personel.txt");

            // Her bir satır ele alınır
            foreach (string person in persons)
            {
                // Satır | işaretine göre ayrıştırılır
                string[] values = person.Split('|');

                // Ayrıştırma sonucu elde edilen değerlere göre Person nesne örneği oluşturulur
                Person prs = new Person
                {
                    PersonId = Convert.ToInt32(values[0]),
                    Name = values[1],
                    Title = values[2],
                    Salary = Convert.ToDecimal(values[3])
                };
                // Persone nesne örneği koleksiyona eklenir
                personList.Add(prs);
                // Console penceresinden bilgilendirme yapılır
                Console.WriteLine("{0} listeye eklendi", prs.Name);

                Thread.Sleep(250); // işleyişi kolay takip edebilmek için küçük bir zaman aldatmacası
            }
            // koleksiyona daha fazla eleman eklenmeyeceğini belirt.
            // Bu metodu kullanmadan denediğinizde programın asılı kaldığını ve kapanmadığını göreceksiniz.
            personList.CompleteAdding();
        }

        // personList isimli generic List koleksiyonundaki her bir Person nesne örneğinin Salary bilgisini değiştirir
        private void ProcessPersonList()
        {
            // Koleksiyondaki her bir Persone nesne örneği ele alınır
            foreach (Person person in personList.GetConsumingEnumerable())
            {
                // O anki Person nesne örneğinin Salary özelliğinin değeri değiştirilir
                person.Salary += 1.18M;

                // Console ekranında bilgilendirme yapılır
                Console.WriteLine("\t {0} için maaş {1} olarak değiştirildi", person.Name, person.Salary);
                Thread.Sleep(250); // işleyişi kolay takip edebilmek için küçük bir zaman aldatmacası
            }
        }
    }
}

Bu kez BlockingCollection<Person> tipinden bir nesne örneğini kullanmaktayız. Bu koleksiyon kendi içerisindeki elemanlar üzerinde eş zamanlı işlemler yapılabilmesine imkan tanımaktadır. Ayrıca istenirse bir boyut verilerek, eş zamanlı çalışma sırasında maksimum eleman ekleme tavanınıda belirtebiliriz. Kodda görüldüğü gibi Task sınıfından yararlanarak kodu tamamen .Net 4.0 havasına büründürmüş bulunuyoruz. Laughing StartTestConcurrent metodu içerisinde dikkat edilmesi gereken noktalardan biriside, Task sınıfının static WaitAll fonksiyonu ile, çalışan tüm Task' lerin tamamlanmasının beklenmesidir. Ayrıca, GetPersonList metodu içerisinde, text tabanlı dosyadaki tüm elemanların aktarılma işlemi tamamlandıktan sonra CompleteAdding fonksiyonu kullanılarak, artık daha fazla eleman eklenmeyeceği, bu nedenle aynı koleksiyon üzerinde bekleyen başka görevler var ise yollarına devam edebilecekleri belirtilmektedir. Eğer CompleteAdding metodunu kullanmassak, programın kapanmadığı gözlemlenecektir. Uygulamayı çalıştırdığımda aşağıdaki sonuçları aldığımı gördüm;

 

Harika değil mi? Laughing Artık hata mesajı yok. Üstelik koleksiyon üzerinde aynı anda iki farklı gövde işlem yapabilmekte. İstenirse görev sayısı dahada arttırılabilir elbetteki. Örneğin çalışmasına göre bir GetPersonList bir ProcessPersonList metodundan sonuçlar alınması Thread.Sleep sürelerinin aynı olmasından kaynaklanmaktadır. Elbetteki gerçek hayat senaryosunda bu süre aynı olmayacaktır. Bende bu düşünce ile Thread.Sleep metodlarını kaldırdığıma aşağıdaki sonuçları aldım.

Dikkat edileceği üzere dosyadan koleksiyona ekleme işlemleri gerçekleşmeden, maaş bilgilerinin düzenlenmesine izin verilmemektedir. Bir başka deyişle koleksiyon içerisinde elemanlar olduğu sürece, ProcessPersonList metodu içerisindeki foreach döngüsü çalışabilmektedir. Aksi durumlarda, koleksiyon üzerindeki iterasyon elemanlar ekleninceye kadar duraksatılmaktadır(Tabi, maaş değişiklikerini yapan foreach döngüsü nerede duracağını nasıl bilecektir sorusunun cevabı = CompleteAdding metodudur). Buda koleksiyona neden BlockingCollection dendiğini açıklamaktadır. Wink

BlockingCollection<T> tipinin farklı özellikleride bulunmakta. Bunlarıda yeri geldikçe incelemeye gayret edeceğim. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

ConcurrentCollections2.rar (27,67 kb)

Concurrent Collections (Eş Zamanlı Koleksiyonlar) [Beta 1]

Cumartesi, 13 Haziran 2009 01:20 by bsenyurt

Merhaba Arkadaşlar,

.Net Framework 4.0 ve içerdiği paralel genişletmeler(Parallel Extensions) ile birlikte gelmekte olan yenilikler arasında, eş zamanlı(Concurrent) çalışabilen ve Thread Safe olan koleksiyonlarda bulunmaktadır. Bu koleksiyonlar aslında veri yapıları(Data Structures) ile birlikte gelen yeni tipler arasında yer almaktadır. Geçtiğimiz günlerde çok şanslı bir insan olarak hafta sonumu bir tatil beldesinde geçirirken,

bu kez gecenin derin sessizliğinde araştırmaya başladığım konulardan biriside işte bu yeni koleksiyonlar oldu. Bu koleksiyon tipleri elbetteki relase sürümünde değişikliğe uğrayabilir. Wink Söz konusu koleksiyon tipleri esasında System.Collections.Concurrent isim alanı altındadır. Ancak bu isim alanı System ve Mscorlib olmak üzere iki assembly içerisine aşağıdaki şekilde görüldüğü gibi dağılmıştır.

Visual Studio 2010 Beta 1 üzerindeki object browser yardımıyla söz konusu tiplere baktığımda bana tanıdık gelebilecek olanlar sadece ConcurrentDictionary, ConcurrentQueue ve ConcurrentStack koleksiyonlarıydı. Nitekim bu tipler daha önceki .Net sürümlerinden bildiğimiz Dictionary, Queue ve Stack koleksiyonlarının eş zamanlı çalışabilen versiyonlarıydı. Ancak kafamda iki önemli soru bulunmaktaydı. Bir; diğer koleksiyon tipleri nasıl ve hangi amaçlar ile kullanılmaktaydı ve iki; koleksiyonların eş zamanlı olmasının ne anlamı vardı Smile

Paralel genişletme ile gelen koleksiyonların ataları çoğunlukla Thread Safe yapıda değildir. Bu nedenle geliştiricinin Thread Safe yapısını sağlaması gerektiği durumlarda kolları sıvaması ve kilitleme mekanizmalarını bilinçli olarak kullanması gerekmektedir. Bir başka deyişle, koleksiyon içerisine dahil edilen elemanlar üzerinde bir iterasyon yapıldığında, başka Thread' ler üzerinden aynı koleksiyonun elemanlarına ulaşmak güvenli değildir. Bu nedenle örneğin bir koleksiyonun elemanları dolaşılırken belirli kriterlere göre aynı koleksiyondan eleman çıkartılmasıda mümkün değildir.(Ki bu durumda geliştiricilerin multi-thread yapıları içerisinde ele alınan koleksiyonlar için senkronizasyon tekniklerini kullanarak sorunu çözmesi gerekmektedir) Hatta aşağıdaki kod parçasında olduğu gibi bir koleksiyonun üyelerinin dolaşılması sırasında,

static void Main(string[] args)
{
 Dictionary<int, string> numbers = new Dictionary<int, string>
 {
  {1,"Bir"},
  {2,"İki"},
  {3,"Üç"},
  {4,"Dört"},
  {5,"Beş"},
  {6,"Altı"}
 };
 
 foreach (KeyValuePair<int,string> number in numbers)
 {
  numbers.Remove(number.Key);
  Console.WriteLine("{0} çıkartıldı.",number.Key);
 }
}

eleman çıkartma işlemi gerçekleştirildiğinde çalışma zamanında aşağıdaki ekran görüntüsünde yer alan InvalidOperationException istisnasını almamız kaçınılmazdır.

Görüldüğü gibi ilk eleman çıkartıldıktan sonra koleksiyonun boyutu değiştiğinden InvalidOperationException istisnasının fırlatılması söz konusu olmuştur. Oysaki Dictionary<T,K> koleksiyonu yerine Concurrent versiyonu kullanılsaydı Thread Safe kuralları çerçevesinde herhangibir sorun ile karşılaşılmazdı. Aşağıdaki kod parçasında bu duruma ait bir kod parçası görülmektedir.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Concurrent;

namespace BlockingCollection
{
    class Program
    {
        static void Main(string[] args)
        {          
            #region Concurrent versiyonu

            ConcurrentDictionary<int, string> numbers = new ConcurrentDictionary<int, string>();
            numbers.TryAdd(1, "Bir");
            numbers.TryAdd(2, "İki");
            numbers.TryAdd(3, "Üç");
            numbers.TryAdd(4, "Dört");
            numbers.TryAdd(5, "Beş");
            numbers.TryAdd(6, "Altı");

            foreach (KeyValuePair<int,string> number in numbers)
            {
                string value;
                bool result=numbers.TryRemove(number.Key, out value);
                if(result)
                    Console.WriteLine("{0} çıkartıldı.",value);
            }

            #endregion
        }
    }
}

ve sonuç;

Görüldüğü gibi koleksiyon elemanları foreach döngüsü ile gezilirken teker teker çıkartılma işlemi yapılabilmiştir. Buna göre öyle vakalar olmalıdır ki, koleksiyonları ele alan paralel süreçlerin aynı örnek üzerindeki elemanlarda Thread Safe kuralları çerçevesinde ekleme, silmve ve güncelleme gibi işlemler yapılabilmelidir. Dolayısıyla paralel genişletmelere ait veri yapılarında yer alan Concurrent koleksiyonların temel kullanım amacı belkide bu şekilde ifade edilebilir. Ben tabiki hemen diğer koleksiyonları ve kullanım amaçlarını merak etmeye başladım ve incelemeye karar verdim. Ne varki içimden bir dürtü, "bak Burakcığım, Thread Safe kolayca bertaraf edilmiş, eş zamanlı olarak aynı koleksiyon üzerinde birden fazla sürecin işlem yapabilmesi sağlanmış. Peki ya performanstan ne haber?" Laughing  Bu nedenle .Net 4.0 öncesi Dictionary koleksiyonu ile ConcurrentDictionary koleksiyonu arasındaki performans farklılıklarını analiz etmeye karar verdim. Aslında ilk tahminlerimin doğru çıktığını ifade edebilirim şimdiden Sealed

Thread Safe + aynı anda ilerleme,ekleme, çıkartma, düzenleme yapabilme yeteneği = pahalı maliyet

İşte test programı kodları;

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

namespace BlockingCollection
{
    class Program
    {
        static void Main(string[] args)
        {
            //Dictionary ve ConcurrentDictionary koleksiyonları için arka arkaya 10 test yapılır
            for (int i = 0; i < 10; i++)
            {
                DictionaryTest();
                ConcurrentDictionaryTest();
                ParallelConcurrentTest();
            }
        }

        static int length = 9000000;

        // Dictionary<int,int> koleksiyonuna eleman ekleme ve okuma işlemlerini ele alır.
        static void DictionaryTest()
        {
            Stopwatch watch = Stopwatch.StartNew();

            Dictionary<int, int> collection = new Dictionary<int, int>();

            // Eleman ekleme işlemi
            Random rnd = new Random();
            for (int i = 0; i < length; i++)
            {
                collection.Add(i, rnd.Next(1, 1000000));
            }
            watch.Stop();
            Console.WriteLine("{0}",watch.Elapsed.TotalSeconds.ToString());

            // Zamanlayıcı sıfırla ve yeniden başlat.
            watch.Reset();
            watch.Start();

            // Eleman okuma işlemi
            foreach (KeyValuePair<int,int> item in collection)
            {
                int value = item.Value;
            }
            watch.Stop();
            Console.WriteLine("{0}", watch.Elapsed.TotalSeconds.ToString());
        }      

        // ConcurrentDictionary<int,int> koleksiyonuna eleman ekleme ve okuma işlemlerini ele alır
        static void ConcurrentDictionaryTest()
        {
            Stopwatch watch = Stopwatch.StartNew();

            ConcurrentDictionary<int, int> collection = new ConcurrentDictionary<int, int>();

            // Eleman ekleme işlemleri
            Random rnd = new Random();
            for (int i = 0; i < length; i++)
            {
                collection.TryAdd(i, rnd.Next(1, 1000000));
            }
            watch.Stop();
            Console.WriteLine("\t{0}", watch.Elapsed.TotalSeconds.ToString());

            // Zamanlayıcıyı sıfırla ve yeniden başlat
            watch.Reset();
            watch.Start();
            // Eleman okuma işlemleri
            foreach (KeyValuePair<int, int> item in collection)
            {
                int value = item.Value;
            }
            watch.Stop();
            Console.WriteLine("\t{0}", watch.Elapsed.TotalSeconds.ToString());
        }

        // Parallel.For ve Parallel.ForEach kullanıldığında Concurrent koleksiyonun eleman ekleme ve okuma işlemlerini test eder.
        static void ParallelConcurrentTest()
        {
            Stopwatch watch = Stopwatch.StartNew();

            ConcurrentDictionary<int, int> collection = new ConcurrentDictionary<int, int>();

            // Eleman ekleme işlemleri
            Random rnd = new Random();

            // Paralel çalışan For döngüsü
            Parallel.For(0, length, i =>
            {
                collection.TryAdd(i, rnd.Next(1, 1000000));
            }
            );
            watch.Stop();
            Console.WriteLine("\t{0}", watch.Elapsed.TotalSeconds.ToString());

            // Zamanlayıcıyı sıfırla ve yeniden başlat
            watch.Reset();
            watch.Start();
            // Eleman okuma işlemleri
            // Paralel çalışan ForEach döngüsü
            Parallel.ForEach<KeyValuePair<int, int>>(collection, item =>
            {
                int value = item.Value;
            }
            );
            watch.Stop();
            Console.WriteLine("\t{0}", watch.Elapsed.TotalSeconds.ToString());
        }
    }
}

Uygulamamızda Dictionary<int,int> ve ConcurrentDictionary<int,int> tipinden iki koleksiyon 3 farklı test metodu yardımıyla ele alınmaktadır. Testler sırasında her iki koleksiyonada rastgele sayılardan oluşan 9000000 tam sayı ilave edilmektedir. Sonrasında ise doldurulan koleksiyonlar ileri yönlü bir iterasyon ile okunmaktadır. Program kodunun temel amacı, eleman ekleme ile okuma işlemlerinde, Dictionary ve ConcurrentDictionary koleksiyonlarının söz konusu işlemleri ortalama olarak ne kadar sürelerde tamamladıklarının testini yapmaktır. ParallelConcurentTest isimli metod dikkat edileceği üzere TPL(Task Parallel Library) kütüphanesinde yer alan Parallel.For ve Parallel.ForEach metodlarını kullanarak ConcurrentDictionary koleksiyonunu ele almaktadır. Ben bu programı intel tabanlı çift çekirdek işlemcili, 4 Gb Ram belleğe sahip ve Vista Enterprise işletim sistemi üzerinde koşturduğumda anlık koşullara göre aşağıdaki ekleme sürelerini tespit ettim.

Eleman Ekleme Süreleri
Deneme Dictionary<int,int> ConcurrentDictionary<int,int> Parallel
1 1,5965157 8,6457496 9,7127165
2 1,7207327 8,6280703 8,8890291
3 1,7718992 8,6033512 9,246576
4 1,9256235 8,7227608 9,4900385
5 1,9287144 8,4039116 9,539486
6 2,0223963 8,6328307 9,6052221
7 1,9426832 10,3117767 11,4428462
8 2,0062376 10,2670853 11,3882937
9 1,9487786 9,7330822 10,8873102
10 1,8028344 10,4151047 11,3630567

Grafik olarak baktığımızda,

ConcurrentDictionary koleksiyonu için eleman ekleme sürelerinin gerçekten çok kötü olduğu gözlemlenebilir. Hatta durumu kurtarmak adına Parallel.For ve Parallel.ForEach metodlarının kullanıldığı durumdaki zaman değerleride son derece kötüdür. Diğer yandan, oluşturulan bu koleksiyonların tüm elemanlarını ileri yönlü bir iterasyon ile dolaştığımızda aşağıdaki zaman değerlerini elde ettiğimi gördüm.

Eleman Okuma Süreleri
Deneme Dictionary<int,int> ConcurrentDictionary<int,int> Parallel
1 0,2707316 0,5216791 0,7073974
2 0,2715216 0,4951542 0,7149783
3 0,3506021 0,5100271 0,7525682
4 0,3380284 0,4933783 0,7305076
5 0,338477 1,3850732 0,7164944
6 0,322663 0,4776662 0,7548498
7 0,2821501 0,5871846 0,8353176
8 0,3824846 0,8149798 0,8492322
9 0,305484 0,577625 0,9152573
10 0,3560983 0,5122665 0,8599752

Duruma grafiksel olarak baktığımızda,

ConcurrentDictionary ve Dictionary arasındaki sürelerin birbirlerine yaklaştıklarını görebiliriz. Ancak ConcurrentDictionary koleksiyonu için okuma sürelerinin(işlemler paralel halde ele alınsalara dahi) yinede Dictonary koleksiyonuna göre belirgin ölçüde yavaş olduğu açıktır.

Elbetteki bu testler, henüz relase edilmemiş olan beta 1 sürümü üzerinden yapılmaktadır. Dolayısıyla zaman içerisinde iyileştirmelerin olması muhtemeldir. Hatta söz konusu uygulamanın çekirdeği yeniden yazılmış olan Windows 7 işletim sisteminde test edilmeside mutlaka gereklidir. Ancak, Concurrent koleksiyonların kullanılma sebeplerinin başında hız veya performans olmadığı gayet net bir biçimde ortadadır. Tabiki bunun dışında kalan senaryolardada gerçekten performans kaybını göze almamızı gerektirecek durumlar olmalıdır. Şu anda sesli düşünüyorum; "Bir uygulama içerisindeki birden fazla tipin ortaklaşa kullandığı bir koleksiyon üzerinde, eş zamanlı olarak ekleme, silme ve düzenleme işlemeleri yapılabiliyor olsun..." Bilmiyorum siz ne düşünüyorsunuz. Aslında fikirlerinizi yorum olarak paylaşabilirsiniz.

Concurrent koleksiyonlar ile ilişkili araştırmalarım devam etmekte. Örneğin şu sıralar göz kestirdiklerimden birisi olan ve aslında bu yazıda incelemek isteyipte, performans ve hız kriterine takıldığım için araştıramadığım BlockingCollection. Bunuda bir sonraki yazımda ele almaya gayret ediyor olacağım. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

ConcurrentCollectionTest.rar (23,87 kb)