Paralel Programlama

Paralel Programlama

logo

Bu yazımızda paralel işlemlerin alt yapısını oluşturan Task Parallel Library (TLP) konusunu incelemeye çalışacağız. Paralel programlama konusuna çok detaylı giriş yapmayacağız. Teorik olarak,işin iş parçacıklarına bölünerek, farklı threadler arasında paylaştırılmasıdır. Böylece daha hızlı sürede çıktı üretme hedeflenir. Ama düzgün yönetilemeyen paralel programlama hedeflenenin aksine, uygulamanın daha kötü performans vermesine sebebiyet verebilir.

Hatta öyle an olur ki, sıralı yöntemlerde elde ettiğiniz çıktı, doğru yönetilemeyen paralel programlanan uygulama çıktısıyla farklı olabilir.

C# dilinden bahsedersek; çok kanallı programlar (Multiple threads), parallel tasks, background worker v.b. yapılar, çoğu programcının bir şekilde aşina olduğu yapılardır. Lock objects ler, autoreset, manual reset eventler, mutexler, semaphore lar v.b. yapılar da işin içine girince, paralel programlama biraz yorucu hal alabilmektedir.
Önemli olan nokta, yapılan işte gerçek anlamda paralel programlama yöntemine ihtiyaç var mı bunun analizi yapılmalıdır. Örneğin, X ürünü kontrol ünitesine yazılım yaptınız. 2 ürün arasındaki işlem süresi 500 ms diyelim. X ürünü geliyor, ikinci ürünün gelmesi 500 ms sonra. İlk X ürününü geldi,

  • Görüntüsünün alınması 80 ms,
  • İşlenmesi 100 ms ,
    toplamda 180 ms’de sisteme işlendi ya da işlenmedi bilgisini dönüyoruz. Geriye 320 ms bekleme süresi kaldı. Böyle bir durumda gerçek anlamda paralel programlamaya gerek var mı ?

Task Parallel Library

TPL kavramını .Net Framework 4.0 ile tanımaya başlıyoruz. Bu yapı, işlemlerin paralel bir şekilde yürütülmesini sağlamak amacıyla oluşturulmuştur. TLP’ye ait tipler System.Threading ve System.Threading.Tasks isim alanında bulunmaktadır.

TLP altyapısı aslında süreçlere, görev olarak bakar , TPL ile;

  • Yeni görevler oluşturmak, bu görevleri başlatmak, duraklatmak ve sonlandırmak mümkündür.
  • Bir görevin bittiği yerden başka bir görevi başlatmak mümkündür.

  • Başarıyla yerine getirilen görevlerin sonucunda değerler döndürmek mümkündür.

  • Bir görev kendi içinde alt görevler başlatabilir.

  • Görevler aynı veya farklı thread’ler tarafından yerine getirilebilirler.

1. Senaryo

Öğrenci Veritabanından, öğrenci tablosundan öğrenci verilerini alarak, her bir öğrenci için pdf formatında özet raporu oluşturan uygulamanın hem klasik yöntem hem de paralel yöntem ile geliştirilmesi.
Öncelikle hızlı anlatım olması bakımından öğrenci isminde liste tanımlayıp, random olarak kayıt oluşturarak, oluşturulan öğrenci bilgilerini pdf formatında özet rapor haline getireceğiz.



    class Program
    {
        static void Main(string[] args)
        {

            List<Ogrenci> ogrenciListesi = Helper.OgrenciOlustur(100);
            Stopwatch watcher = new Stopwatch();
            watcher.Start();
            SeriOlustur(ogrenciListesi);
            watcher.Stop();
            Console.WriteLine("Seri  hesaplama geçen süre {0}",watcher.ElapsedMilliseconds);
            //paralel
            watcher.Restart();
            ParalelOlustur(ogrenciListesi);
            watcher.Stop();
            Console.WriteLine("Paralel hesaplama geçen süre {0}",watcher.ElapsedMilliseconds);
            Console.ReadKey();

        }

        static void SeriOlustur(List<Ogrenci> ogrenciler)
        {
            foreach (var ogrenci in ogrenciler)
            {
                CreatePdf(ogrenci);
            }
        }

        static void ParalelOlustur(List<Ogrenci> ogrenciler)
        {
         
            Parallel.ForEach(ogrenciler, o => CreatePdf(o));
        
        }

        static void CreatePdf(Ogrenci ogrenci)
        {   
            //Döküman oluşturma
            PdfDocument document = new PdfDocument();
            document.Info.Title = "Öğrenci Durum Belgesi";
            //dökümana sayfa ekleme
            PdfPage page = document.AddPage();
            page.Size = PageSize.A4;
            page.Orientation = PdfSharp.PageOrientation.Landscape;
            string icerik = "Ogrenci Adı =" + ogrenci.Adi + System.Environment.NewLine + "Öğrenci NO=" + ogrenci.OgrenciNo + System.Environment.NewLine + "Bölümü=" + ogrenci.Bolumu;
            XGraphics xGraphics = XGraphics.FromPdfPage(page);

            //Font oluşturma
            XFont font = new XFont("Arial", 12, XFontStyle.Regular);
            PdfSharp.Drawing.Layout.XTextFormatter tformatter = new PdfSharp.Drawing.Layout.XTextFormatter(xGraphics);
            XRect rect = new XRect(20, 20, 400, 170); //yazılacak alan koordinatları


            xGraphics.DrawRectangle(XBrushes.SeaShell, rect);
            tformatter.DrawString(icerik, font, XBrushes.Black, rect, XStringFormats.TopLeft);
            xGraphics.Dispose();
           
            document.Save($"{ogrenci.OgrenciNo}.pdf");
            document.Dispose();

        }
    }

  struct Ogrenci
    {

        public string Adi { get; set; }
        public  long OgrenciNo { get; set; }
        public string Bolumu { get; set; }

    }

    static class Helper
    {

        public static List<Ogrenci> OgrenciOlustur(int ogrenciSayisi)
        {
            Random rd = new Random();
            List<Ogrenci> ogrenciListesi = new List<Ogrenci>();
            string[] bolumler = new string[3] { "Bilgisayar Mühendisliği", "Kimya", "Tıp" };
            for (int i = 0; i < ogrenciSayisi; i++)
            {
                Ogrenci ogrenci = new Ogrenci();
                ogrenci.Adi = $"Ogrenci{i}";
                ogrenci.OgrenciNo = i * 3;
                ogrenci.Bolumu = bolumler[rd.Next(0, 3)];
                ogrenciListesi.Add(ogrenci);

            }
           return ogrenciListesi; 
           
        }
    }
}


Yukarıda yer alan örnek uygulamamızda, main metodu içerisinde, statik olarak tanımladığımız Helper sınıfından faydalanarak , öğrenci listemizi hazırladık.
System.Diagnostic isim uzayından Stopwatch sınıfından türetilen watcher nesnesi ile, process’in başlangıcı ile bitişi arasında geçen süreyi hesapladık.
Akabinde klasik yöntemle foreach döngüsüne soktuk.

  static void SeriOlustur(List<Ogrenci> ogrenciler)
        {
            foreach (var ogrenci in ogrenciler)
            {
                CreatePdf(ogrenci);
            }
        }

daha sonra , watcher nesne değerini resetledik ve akabinde paralel hesaplama yapan metodumuzu çağırdık.

  static void ParalelOlustur(List<Ogrenci> ogrenciler)
        {
         
            Parallel.ForEach(ogrenciler, o => CreatePdf(o));
        
        }

Foreach metoduna baktığımızda şöyle bir imza ile karşılaşıyoruz: Parallel.Foreach(IEnumerable source, Action body)

source : Foreach işlemi boyunca içerisinde dolaşacağımız IEnumerable arayüzünü implemente etmiş olan koleksiyon

body : Foreach döngüsü içerisinde ele alacağımız işlemler.

CreatePDF() methodu ile PDF döküman oluşturma işlemini gerçekleştirdik.PDF işlemleri için, PDFSharp kütüphanesi kullanılmıştır.
Uygulama çıktısı :
cikti
Görüldüğü üzere, paralel programlama, klasik yöntemden 2.5 kat daha hızlı.

Uygulamada bazı düzenlemeler yaparak , 3 farklı örnekleme değeri için uygulama performansına bakalım:

100 Öğrenci cikti

300 Öğrenci
cikti

500 Öğrenci
cikti

Görüldüğü gibi, çalışılan veri miktarı artınca, sıralı yöntem ile paralel yöntem arasındaki performans fark değeri azalsa da , paralel yöntem sıralı yöntemden hala hızlı ..

2. Senaryo

Bu uygulamamızda, hem sıralı hem de paralel yöntem ile hesaplama işlemini gerçekleştireceğiz.

 class Program
    {
        static void Main(string[] args)
        {
            List<int> sayiListesi = Helper.SayılarOlustur(1000);

            // Sıralı yöntem
            SeriHesapla(sayiListesi);

            // Paralel Yöntem
            ParalelHesapla(sayiListesi);
            Console.ReadKey();
        }

        static void SeriHesapla(List<int> sayilar)
        {
            int bolunenler = 0;
            for (int i = 0; i < sayilar.Count; i++)
            {
                if (sayilar[ i] % 13 == 0)
                    bolunenler++;
            }

            Console.WriteLine("Seri yöntem -  13 ile bölünen sayısı = {0}", bolunenler.ToString());

        }

        static void ParalelHesapla(List<int> sayilar)
        {
            int prBolunenler = 0;
            Parallel.For(0, sayilar.Count,i =>
              {
                  if (sayilar[i] / 13 == 0)
                      prBolunenler++;
              }

            );
            Console.WriteLine("Paralel yöntem - 13 ile bölünen sayısı ={0}", prBolunenler.ToString());
        }
    }

    static class Helper
    {
     public  static List<int> SayılarOlustur(int maksDeger)
        {
            List<int> sayilar = new List<int>();
            Random rd = new Random();
            for (int i = 0; i < maksDeger; i++)
            {
                sayilar.Add(rd.Next(1, 500));

            }
            return sayilar;
        }

    }

Uygulama çıktısı :

cikti

Yazımızın giriş cümlelerinde de bahsetmiştik.Bazen öyle durumlar olur ki, klasik yöntem ile paralel yöntem çıktıları birbirinden farklı olabilir. İşte bu durum bunun en güzel örneklerinden. İki metodumuzda aynı değerler üzerinde hesaplama yaptılar ama farklı çıktı ürettiler.Daha hızlı çıktı alma hevesi, yanlış çıktı alma gibi sonucu doğurabilir.

Örnek uygulamamızda Paralel sınıfının For methodu , birden fazla görev(task) oluşturup, bunları birden fazla thread’e çalışması için gönderdi. Bu thread’ler görevleri başlatarak, ortak değişken olan prBolunenler değişkenini artırdılar. Aslında problem tam bu noktada kaynaklanmaktadır. Her thread ortak değişkeni arttırdı ve hatalı sonuç üretilmesine sebebiyet verdi. Böyle durumlarda her thread’in ayrı değişkenler üzerinde çalışmasını sağlayıp, her birinden dönen sonuçları toplayıp, tek değişkene indirmemiz gerekecektir. Bu yönteme İndirgeme (Reduction) denmektedir. Paralel çalışan metodumuz aşağıdaki şekilde yeniden düzenlersek;

   static void ParalelHesapla(List<int> sayilar)
        {
           
            int prBolunenler = 0;
            Parallel.For(0, sayilar.Count, () => 0, (i, loop, threadBolunen) =>
                 {
                     if (sayilar[i] % 13 == 0)
                         threadBolunen++;

                     return threadBolunen;
                 },
                 
                 (threadBolunen)=>   Interlocked.Add(ref prBolunenler, threadBolunen)
                     
                 );
           Console.WriteLine("Paralel yöntem -indirgeme yöntemi- 13 ile bölünen sayısı ={0}", prBolunenler.ToString());
        }

Kodumuza değinirsek Parallel Sınıfını for döngüsünde , indirgeme işlemi için düzenleme yaptık, önceki kodumuza ilave olarak 2 yeni parametre ekledik:
Func<int,ParallelLoopState,TLocal,TLocal> tipinde, paralelloopState ile , thread’in hesaplamada erişeceği kendi lokal değişkenini tanımladık.

(threadBolunen)=> Action tanımı ile de , çalışmasını bitiren thread, kendi lokal değişken değerini, ortak değişken değerine eklemektedir. Bu ekleme işini Interlocked sınıfı senkronize şekilde yapmaktadır.

uygulama çıktısı :

indirgeme

Uygulamalarınızda hangi thread, hangi sonucu buldu vb. işlemlerini izlemek isterseniz, Thread sınıfının ManagedThreadId property’si kullanılılır. Örn. Thread.CurrentThread.ManagedThreadId kodu o anki çalışan thread ID’yi verir; Paralel Programlama desenlerini detaylı incelemek için repomuzda yer alan

https://github.com/Cankirism/Patterns_of_Parallel_Programming_CSharp dökümanı inceleyebilirsiniz.

C# 5.0 ile birlikte gelen,TASK yapısı ile paralel programlamanın farklı bir seviyeye geldiğini söyleyebiliriz.

Sağlıcakla kalın..

December 27, 2019 tarihinde oluşturuldu.