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

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)