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

PLINQ - ForAll [Beta 1]

Perşembe, 28 Mayıs 2009 17:43 by bsenyurt

Merhaba Arkadaşlar,

Bildiğiniz gibi bir süredir LINQ sorgularının paralel çalıştırılması ile ilişkili çalışmalarıma ve araştırmalarıma devam etmekteyim. Bu yazımdaki konumuz ise System.Linq.ParallelEnumerable static sınıfı içerisinde tanımlanmış olan ForAll genişletme metodudur(extension methods).

public static void ForAll<TSource>(this ParallelQuery<TSource> source, Action<TSource> action);

ForAll metodu yukarıdaki prototipinden de görüldüğü gibi ParallelQuery referanslarına uygulanabilmektedir. Bununla birlikte metod ikinci parametre olarak, Action<TSource> tipinden generic bir temsilci almaktadır.

public delegate void Action<in T>(T obj);

Yukarıdaki prototipe göreyse, Action<T> temsilcisi(delegate), generic tip olarak ForAll metoduna gelen tipi(TSource) kullanmaktadır. Bu generic tip tahmin edeceğiniz üzere ParalleQuery referansınında kaynak tipidir. Ayrıca temsilci geriye herhangibir değer döndürmeyen(void) metodları işaret edebilmektedir.

Sonuç olarak ForAll metodu aslında, AsParallel metodunun kullanılması sonucu üretilen referans üzerinden gelen her bir nesne örneği için yapılması istenen işlemleri ele almaktadır. Bu açıdan bakıldığında akla gelen soru şu olacaktır.

Paralel sorguların çalışması sonucu üretilen çıktılar üzerinde foreach döngüleri yardımıyla da dolaşabiliyorken, ForAll metodunu neden kullanırız?

Aşağıdaki kod parçasını göz önüne alalım.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Threading;

namespace UsingForAll
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Product> productList = GetProductList();

            var result = from p in productList.AsParallel()//.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
                         where p.ListPrice>=400 && p.Color=="Black"
                         select p;

            result.ForAll(p=>Console.WriteLine("("+Thread.CurrentThread.ManagedThreadId.ToString()+")\t"+p.Name));
        }

        static List<Product> GetProductList()
        {
            List<Product> productList = new List<Product>();

            SqlConnection conn = new SqlConnection("data source=Manchester;database=AdventureWorks2008;integrated security=true");
            SqlCommand cmd = new SqlCommand("Select ProductId,Name,ListPrice,ProductNumber,Color,SafetyStockLevel From Production.Product", conn);
            conn.Open();
            SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
            while (reader.Read())
            {
                productList.Add(new Product
                {
                    ProductId = Convert.ToInt32(reader[0]),
                    Name = reader[1].ToString(),
                    ListPrice = Convert.ToDecimal(reader[2]),
                    ProductNumber = reader[3].ToString(),
                    Color = reader[4].ToString(),
                    SafetyStockLevel = Convert.ToInt32(reader[5])
                }
                );
            }
            reader.Close();
            return productList;
        }
    }

    class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public decimal ListPrice { get; set; }
        public string ProductNumber { get; set; }
        public string Color { get; set; }
        public int SafetyStockLevel { get; set; }
    }
}

Bu kod parçasında odaklanmamız gereken nokta result referansı üzerinden ForAll metodunun çağırılışıdır. Bu çağrı sırasında labmda operatöründen(=>) yaralanılmaktadır ve bulunan ürünlerin adları ile o an çalışmakta olan Thread' in numarası(ManagedThreadId) Console ekranına yazdırılmaktadır. Sonuç aşağıdakine benzer olacaktır.

Her ne kadar Thread sayıları eşit olmasada 4 ve 1 nolu iki ayrı iş parçasının çalıştırıldığı görülmektedir. Şimdi aynı sorgu sonuçlarını foreach döngüsü yardımıyla elde etmeye çalıştığımızı düşünelim.

List<Product> productList = GetProductList();

var result = from p in productList.AsParallel()//.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
                 where p.ListPrice>=400 && p.Color=="Black"
                 select p;

foreach (Product p in result)
{
   Console.WriteLine("(" + Thread.CurrentThread.ManagedThreadId.ToString() + ")\t" + p.Name);
}

Bu kez uygulama çalıştırıldığında aşağıdaki sonuçları alırız.

Volaaa!!! Cool 1 numaralı sadece tek bir thread görünüyor.

Bu nasıl oldu? Acaba foreach döngüsü kullanıldığında sorgu AsParallel metodu olmasına rağmen paralel çalıştırılmadı mı? Yoksa çalışma zamanı(runtime) sorgunun paralel çalıştırılmaya değer olmadığına mı kanaat getirdi(ki böyle bir meselede var)?

Aslında farklı çalışmanın sebebi şu. LINQ sorguları bilindiği gibi kullanıldıkları yerde çalıştırılırlar(deferred execution ilkesi). Bu nedenle sorgunun çalıştırılması foreach döngüsünde ilk eleman elde edilmeye çalışıldığı sırada olur. Lakin sorgu AsParallel metodu nedeniyle paralel çalışmasına rağmen, foreach metodu okuma işlemine başlamadan önce tüm yönetimli thread' leri tekrardan tek bir thread içerisinde birleştirir. Yani foreach döngüsünün kendisi paralel çalışma özelliğne sahip değildir. Bu nedenle paralel çalıştırılan sorgu sonuçlarını, o an üzerinde çalıştığı thread' de birleştirmeden ilerleyemez. ForAll metodu ise tam aksine çalışmakta olup, okuma işlemlerininde paralel yürütülmesini sağlamaktadır. Aslında durumu basit iki resim ile canlandırmaya çalışalım. Aşağıdaki şekilde foreach çalışması sırasındaki işleyiş sembolize edilmektedir.

Buna göre sorgu paralel olarak çalışan görevlere ayrılmakta ve herbir görev içerisinde where gibi koşullar ele alınmaktadır. Ancak tüm PLINQ ifadesi tamamlandığında sonuçlar tek bir Task altında birleştirilmektedir. (Kahverengi çerçeveli kutucuklar bulunan nesne örnekleri üzerinden yapılan işlemleri sembolize etmektedir. Örneğin Console.WriteLine gibi) Sonrasında ise herbir öğe için foreach döngüsü içerisinde yazılan kodlar işletilmektedir.

Aşağıdaki şekilde ise ForAll kullanımı sırasındaki senaryo ifade edilmeye çalışılmaktadır.

Yine PLINQ ifadesinin çalışması sırasında n adet Task paralel olarak başlatılır. Ancak foreach' ten farklı olarak her task' in içerisinde hem Where gibi koşulların kontrolü ele alınmakta hemde örneğimizde ki her bir sonuç için ayrı ayrı işlemler(Console.WriteLine gibi) gerçekleştirilmektedir. Yani task' ler paralel olarak işledikten sonra tek bir Task altında birleştirilmezler. Sanıyorum şekil yardımıyla sizde benim gibi, gerçekleşen iki farklı işleyişi daha net canlandırabildiniz. (Tabi işlemcinin içerisine girip olan biteni canlı canlı görmemiz mümkün değil. Ama kim bilir, belki gelecek nesil sistemlerde çalışma zamanını, tıpkı bir doktorun sanal bir hastanın organları içerisinde ilerleyişi gibi, bilgisayar donanımı üzerindem gözlemleyebiliriz. Wink)

Herşey güzel ama, hangisini ne zaman kullanmak gerekir öyleyse?

Aslında foreach döngüsünü daha çok sorgu sonuçlarının sırasını(order) korumak istediğimiz durumlarda değerlendirebiliriz. Bununla birlikte, sonuç listesi üzerinde ardışıl olarak işlemler yapmak istiyorsakta tercih edebiliriz.

Böylece geldik kısa bir yazımızın daha sonuna. Bir sonraki yazımızda görüşünceye dek hepinize mutlu günler dilerim.

UsingForAll.rar (22,17 kb)

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

Paralel Sorgularda İstisna Yönetimi(Exception Handling) [Beta 1]

Salı, 26 Mayıs 2009 17:30 by bsenyurt

Merhaba Arkadaşlar,

Yönetimli kod(Managed Code) tarafında istisna yönetimi oldukça önemli konulardan birisidir. Uygulamaların veya kod süreçlerinin istem dışı sonlanmasının önüne geçilmek istendiği durumlarda, basit try...catch...finally bloklarından yararlanabilir yada Enterprise Library gibi kütüphanelerin sunduğu bloklardan faydalanarak istisna yönetimini üst seviyede sağlayabiliriz.

Bu yazımda çok geniş kapsamda düşünmeyip, PLINQ(Parallel Language INtegrated Query) ifadelerinde oluşabilecek istisnai durumların nasıl ele alınması gerektiği üzerinde durmaya çalışacağız. Olaya hızlı bir giriş yapıp aşağıdaki örnek kod parçasına sahip olduğumuzu düşünelim.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;

namespace SequentialPLINQ
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Product> productList = GetProductList();           
            productList[497].SafetyStockLevel = 0;
            productList[503].SafetyStockLevel = 0;

            var result1 = from product in productList.AsParallel()                         
                          orderby product.ProductId
                          where product.ListPrice>=500
                          select new
                          {
                              product.ProductId,
                              product.Name,
                              product.ListPrice,
                              product.Color,
                              SellPrice=FindSellPrice(product.ListPrice,product.SafetyStockLevel)    // İlk durum                         
                          };


            try
            {
                foreach (var r in result1)
                {
                    Console.WriteLine(r.ProductId + " " + r.Name + " (" + r.SellPrice.ToString("C2") + ")");
                }
            }
            catch (AggregateException excp) // Hata oluştuğunda PLINQ içerisinde başlatılan tüm alt işlemler(Threads) iptal edilir
            {
                // PLINQ ifadeleri çalıştırıldığı sırada oluşan istisnalar AggreateException nesne örneği içerisindeki InnerExceptions özelliğinin refere ettiği koleksiyonda toplanırlar.
                foreach (Exception error in excp.InnerExceptions)
                {
                    Console.WriteLine(error.Message);
                }
            }
        }
           static decimal FindSellPrice(decimal listPrice,int stockLevel)
        {
            decimal result=-1;
                if (DateTime.Now.Day >= 25
                    && DateTime.Now.Day <= 28)
                    result=listPrice - (listPrice * (1 / stockLevel)); // SafetyStockLevel' ın 0 gelmesi halinde exception oluşacak olan yer.
                else
                    result= listPrice * 1.18M;
            return result;
        }

        static List<Product> GetProductList()
        {
            List<Product> productList = new List<Product>();

            SqlConnection conn = new SqlConnection("data source=Manchester;database=AdventureWorks2008;integrated security=true");
            SqlCommand cmd = new SqlCommand("Select ProductId,Name,ListPrice,ProductNumber,Color,SafetyStockLevel From Production.Product", conn);
            conn.Open();
            SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
            while (reader.Read())
            {
                productList.Add(new Product
                {
                    ProductId = Convert.ToInt32(reader[0]),
                    Name = reader[1].ToString(),
                    ListPrice = Convert.ToDecimal(reader[2]),
                    ProductNumber = reader[3].ToString(),
                    Color = reader[4].ToString(),
                    SafetyStockLevel = Convert.ToInt32(reader[5])
                }
                );
            }
            reader.Close();
            return productList;
        }
    }

    class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public decimal ListPrice { get; set; }
        public string ProductNumber { get; set; }
        public string Color { get; set; }
        public int SafetyStockLevel { get; set; }
    }
}

Visual Studio 2010 Professional Beta 1 ile geliştirilen bu kod parçasında, Product isimli bir sınıftan yararlanılmaktadır. Product tipine ait veriler, SQL sunucusu üzerindeki Product tablosundan alındıktan sonra, generic List<Product> koleksiyonu içerisinde tutulmakta ve sonrasında ise paralel sorgulamaya tabi tutulmaktadır. Burada ayrıca dikkat edilmesi gereken bir noktada, sorgulama sırasında FindSellPrice isimli metodun çağırılması ve parametre olarak, sorgunun t anındaki Product nesnesine ait ListPrice ile StockLevel değerlerinin gönderilmesidir. Dikkat edileceği üzere FindSellPrice metodu içerisinde güne göre ürünlerde indirim uygulanmasını hedef alan bir formül yer almaktadır. Formülü tamamen kafadan uydurduğumu ifirat etmek isterim. Zaten sizde bunu anlamışsınızdır. Wink Asıl varmak istediğim nokta, StockLevel değerlerinin 497 ve 503 nolu ürünler için bilinçli olarak sıfıra set edilmiş olmasıdır. Bu nedenle bölme işlemi sırasında bir istisna oluşması kaçınılmazdır.

(Yani kendi kendimize kaşınıp kod içerisine bir bubi tuzağı koymuş durumdayız. 

Önemli olan nokta, paralel sorgu motorunun bu tip bir durum ile karşılaştığında ne yapacağıdır. Nitekim söz konusu sorgulama tekniğine göre, operasyon bir kaç parça Thread' e bölünmete ve bu nedenle oluşacak bir istisnada(veya istisnalarda) çalışan iş parçalarına ne olacağı sorusu akla gelmektedir. İşte kodun yukarıdaki halinin çalışması sonrası ekran görüntümüz.

Görüldüğü gibi hiç bir Product bilgisi ekrana çıktı olarak gelmemiştir. Bu son derece doğaldır. Nitekim paralel sorgulama motoru herhangibir istisna ile karşılaştığında, çalışmakta olan tüm Thread' lerin iptal edilmesini sağlamaktadır. Diğer yandan istisna nesnesinin tipine dikkat edilmelidir. PLINQ ifadeleri içerisinde meydana gelebilecek istisnalar, AggregateException istisna sınıfı tarafından sarmalanmaktadır. Birden fazla istisna olabileceğinden, AggregateException sınıfı InnerExceptions isimli birde özelliğe sahiptir. Dolayısıyla catch bloğu içerisinde, sıfıra bölem hatasının yakalanması için, InnerExceptions koleksiyonunda dolaşılması gerekemtekdir. (InnerExceptions özelliği ReadOnlyCollection<Exception> tipinden bir koleksiyon döndürmektedir.)

Peki ya, istisna olan parçaların atlanması(bu örneğe göre) istenirse. Bir başka deyişle istisna almayan parçaların yinede paralel sorgulama sonucu ele alınması istenirse ne yapabiliriz?

Bu sorunun cevabı son derece basittir aslında. Exception yönetimi, FindSellPrice isimli metod içerisinde gerçekleştirilir.

static decimal FindSellPrice(decimal listPrice,int stockLevel)
{
    decimal result=-1;
    try 
    {
    if (DateTime.Now.Day >= 25 && DateTime.Now.Day <= 28)
       result=listPrice - (listPrice * (1 / stockLevel)); // SafetyStockLevel' ın 0 gelmesi halinde exception oluşacak olan yer.
    else
       result= listPrice * 1.18M;
    }
    catch(DivideByZeroException excp)
    {
      Console.WriteLine("\tStok miktarı 0 olduğundan satış fiyatı hesaplanamadı");
    }
    return result;
}

Bu durumda uygulama başarılı bir şekilde çalışacak ve aşağıdaki ekran görüntüsüne benzer sonuçlar alınacaktır.

Görüldüğü gibi istisnaya neden olan ürünler kontrollü bir şekilde elenmiş ve sorgunun paralel olarak yürütülmesine devam edilebilmiştir. Böylece geldik bir yazımızın daha sonuna. Bu yazımızda PLINQ sorgularında, istisna yönetiminde nelere dikkat etmemiz gerektiğine değinmeye çalıştık. Tekrardan görüşünceye dek hepinize mutlu günler dilerim. 

ExceptionHandling.rar (27,33 kb)

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

PLINQ - Paralellik Altında Ardışık(Sequential) Çalışmak [Beta 1]

Pazartesi, 25 Mayıs 2009 23:34 by bsenyurt

Merhaba Arkadaşlar,

Bir önceki blog yazımızda PLINQ ifadelerinde sıralama konusuna değinmeye çalışmıştık. Bu yazımızda ise, paralel olarak çalıştırılan LINQ sorguları içerisinde, ardışık(Sequential) olarak nasıl işlem yapılabileceğini incelemeye çalışacağız.

PLINQ ifadeleri, sorgu içerisindeki işlemleri paralel çalışan görevlere ayırmakta son derece başarılıdır. Ancak öyle senaryolar olabilirki, sorgunun belirli bir noktasından(noktalarından) sonra ardışık olarak işlemlerin devam etmesi istenebilir.(Hatta sonra tekrardan paralel olarak devam edilmeside sağlanabilir) Tabi bu şekilde anlatmaya çalışınca inanın benim kafamda karışıyor. Undecided Gelin olayı basit bir örnek ile ele almaya çalışalım. İşte Visual Studio 2010 Professional Beta 1 üzerinde geliştirdiğim Console uygulamasına ait kodlar.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;

namespace SequentialPLINQ
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Product> productList = GetProductList();

            int tid= 0;
            var result1 = from product in productList.AsParallel()
                          where product.Color.StartsWith("B")
                          orderby product.ProductId
                          select new
                          {
                              Id = tid++,
                              product.ProductId,
                              product.Name,
                              product.ListPrice,
                              product.Color
                          };
                         

            foreach (var r in result1)
            {
                Console.WriteLine(r.Id+" \t"+r.ProductId+" "+r.Name+" "+r.Color);
            }
        }

        static List<Product> GetProductList()
        {
            List<Product> productList = new List<Product>();

            SqlConnection conn = new SqlConnection("data source=Manchester;database=AdventureWorks2008;integrated security=true");
            SqlCommand cmd = new SqlCommand("Select ProductId,Name,ListPrice,ProductNumber,Color,SafetyStockLevel From Production.Product", conn);
            conn.Open();
            SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
            while (reader.Read())
            {
                productList.Add(new Product
                {
                    ProductId=Convert.ToInt32(reader[0]),
                    Name=reader[1].ToString(),
                    ListPrice=Convert.ToDecimal(reader[2]),
                    ProductNumber=reader[3].ToString(),
                    Color=reader[4].ToString(),
                    SafetyStockLevel=Convert.ToInt32(reader[5])
                }
                );
            }
            reader.Close();
            return productList;
        }
    }

    class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public decimal ListPrice { get; set; }
        public string ProductNumber{ get; set; }
        public string Color { get; set; }
        public int SafetyStockLevel { get; set; }
    }
}

Örnekte SQL Server 2008 üzerinde kurulu olan, AdventureWorks2008 veritabanındaki Production şemasında yer alan Product tablosuna ait veriler kullanılmaktadır. Product tablosunun kod içerisindeki temsili için, Product isimli bir sınıf tasarlanmıştır. Sınıfa ait nesne örneklerinden oluşan generic List<Product> koleksiyonunun doldurulması için GetProductList metodundan yararlanılmaktadır. Söz konusu generic liste PLINQ ifadesi yardımıylada sorgulanmaktadır. Buraya kadarki kısımda zaten ilginç bir şey yok. Dikkat edeceğimiz nokta, isimsiz tip(Anonymous Type) içerisinde tid isimli sayacın arttırılmasıdır. Wink Öyleki uygulamayı çalıştırdığımızda aşağıdaki sonuçları elde ederiz.

Burada dikkat edilmesi gereken nokta tid değerlerinin ardışık mantığa göre değil, paralel çalışmanın bir sonucu olarak farklı sıralarda üretilmesidir. Bu nedenle 1, 3, 5, 8, 74... gibi bir dizi oluşmuştur. Bu dizi kodun her çalıştırılmasında farklı şekillerde üretilebilir. Örneği ikinci kez çalıştırdığımda bu kez aşağıdaki sonuçları aldım.

Görüldüğü üzere tid değerlerinin arttırımının, çıktıya yansıyışı farklı olmaktadır. Hatta MSDN kaynaklarında, arttırımı yapılan değerlerin tekrar etmesininde mümkün olabileceği berlitilmektedir. İşte yazmış olduğum bu anlamsız örnekteki gibi (örneğin arttırım işlemlerinin ardışık bir şekilde gerçekleştirilmesinin gerektiği vb...), paralel olarak çalışan LINQ sorguları içerisinde, ardışık çalışması gereken bölümler var ise, AsSequential genişletme metodunun(Extension Method) kullanılması gerekmektedir. Buna göre yukarıdaki örnekte yer alan LINQ sorgusunu,

var result1 = productList
                .AsParallel()
                .Where(p => p.Color.StartsWith("B"))
                .OrderBy(p => p.Name)
                .AsSequential()
                .Select(
                p => new
            {
                Id = tid++,
                p.ProductId,
                p.Name,
                p.ListPrice,
                p.Color
            });

şeklinde değiştirir ve örneği tekrar çalıştırırsak aşağıdaki sonuçları elde ederiz.

Görüldüğü gibi tid arttırımı düzenli bir şekilde çıktıya yansıtılmıştır. (Yinede dikkatlice baktığınızda Name özelliğine göre yapılan sıralamalarda, her iki LINQ sorgusu arasında küçük farklar olabileceğini görebilirsiniz)

Sonuç olarak paralel olarak çalıştırılan LINQ sorguları içerisinde, ardışık yürütülmesi gereken operasyonlar var ise bu durumda AsSequential genişletme metodnun kullanılması gerekmektedir. Diğer taraftan elbetteki bu kullanım, söz konusu paralel çalışmanın hızını düşürecek bir etkiye neden olacaktır. Bu da gözden kaçırılmaması gereken diğer bir noktadır.

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

SequentialPLINQ.rar (25,80 kb)

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

PLINQ - Sıralamayı(Ordering) Korumak [Beta 1]

Pazar, 24 Mayıs 2009 05:30 by bsenyurt

Merhaba Arkadaşlar,

Hatırlayacağınız gibi, PLINQ(Parallel LINQ) ile ilişkili ilk yazımda, LINQ sorgularının eş zamanlı olarak nasıl çalıştırılabileceğini incelemeye çalışmıştık. Hello World örneğimizde ağırlıklı olarak aşağıdaki sorgu üzerinde durmuştuk.

var result2 = from p in products.AsParallel()
                          where p.ListPrice >= 10 && p.InStock==true
                          orderby p.Name descending
                          select p;

Bu sorguda yer alan orderby kelimesi aslında çok büyük bir öneme sahiptir. Gelin ne demek istediğimi size anlatmaya çalışayım. Yine Visual Studio 2010 Professional Beta 1 ortamında geliştirilen aşağıdaki kod parçasına sahip bir Console uygulamamız olduğunu göz önüne alacağız.

using System;
using System.Collections.Generic;
using System.Linq;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Product> products = FillProducts();
            Console.WriteLine("Liste dolduruldu. İşlemlere devam etmek için tıklayın");
            Console.ReadLine();

            Console.WriteLine("Listenin ilk hali");
            foreach (Product prd in products)
            {
                if(prd.InStock==true)
                    Console.WriteLine(prd.Name);
            }

            var result = from p in products.AsParallel()
                         where p.InStock == true
                         select p;

            Console.WriteLine("AsParalell sonrası listenin hali");
            foreach (Product prd in result)
            {
                Console.WriteLine(prd.Name);
            }
        }

        static List<Product> FillProducts()
        {
            List<Product> products = new List<Product>();

            for (long i = 1; i < 21; i++)
            {
                Product prd = new Product
                {
                    Id = i
                    ,Name = "Product" + i.ToString()
                    ,ListPrice = i * 0.1M
                    ,InStock = i % 2 == 0 ? true : false
                };
                products.Add(prd);
            }

            return products;
        }
    }


    class Product
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public decimal ListPrice { get; set; }
        public bool InStock { get; set; }
    }
}

Bu seferki örneğimizde temel amacımız hız veya işlemlerin daha kısa sürede tamamlanması değildir. products isimli generic List koleksiyonu doldurulduktan sonra bir foreach döngüsü yardımıyla dolaşılmakta ve stokta olanlar(InStock==true) ekrana yazdırılmaktadır. Sonrasında ise PLINQ sorgumuz gelmekte ve aynı sonuçları paralel çalışan task' ler üzerinde elde etmemizi sağlamaktadır. Hemen küçük bir dip not belirtelim; LINQ sorguları aslında kullanıldıkları anda çalıştırılmaktadır(Deferred Execution). Yani çalışma zamanında ikinci foreach döngüsüne gelindiğinde, söz konusu PLINQ sorgusu yürütülmekte ve dolayısıyla paralel görevler devreye girmektedir. Peki herşey güzel hoş... Hoş ama niye bunun üzerinde duruyoruz? Aslında çalışma zamanındaki ekran görüntüsü herşeyi biraz olsun açıklıyor.

Dikkat edileceği üzere, listenin ilk halinde stokta olan ürünler, koleksiyonda yer aldıkları sıraya göre ekrana getirilmektedir. Ancak AsParallel genişletme metodunun kullanılması sonrasında elde edilen listedeki ürünler sıralı bir şekilde gelmemektedir. Bu çok doğal olarak paralel çalışmanın bir sonucudur. Ancak bazı hallerde AsParallel kullanımı sonrasında, kaynak listenin sıralı olarak elde edilmesi istenebilir. Bu durumda orderby kullanımı sorunu çözecektir. Söz gelimi yukarıdaki örnekte yer alan LINQ ifadesinde orderby aşağıdaki gibi kullanılabilir.

var result = from p in products.AsParallel()
                         where p.InStock == true
                         orderby p.Name
                         select p;

Bu durumda örneğin çalışması sonucu aşağıdaki çıktı elde edilir.

Ancak PLINQ tipinden sorgunun çalıştırılması sırasında orjinal nesne sırasının korunması da istenebilir ki bu orderby kullanımından daha farklı anlamdadır. (orderby kullanımında listenin, koleksiyondaki orjinal sırası yerine, sıralama kriteri belirtilir.) İşte bu noktada devreye ParallelEnumerable static sınıfı içerisinde tanımlanmış olan AsOrdered isimli genişletme metodu(Extension Method) girer. Yani sorguyu aşağıdaki hale getirirsek, liste elemanlarının koleksiyon içerisindeki orjinal sırasını koruyarak sonuç elde edilmesi sağlanabilir.

var result = from p in products.AsParallel().AsOrdered()
                         where p.InStock == true
                         select p;

Ve sonuç...

Tabi burada önemli bir nokta daha vardır. AsOrdered çok doğal olarak PLINQ sorgunun çalışma zamanında yavaş işlemesine neden olacaktır. Çünkü, sorgu sonucu elde edilen liste eşitliğin sol tarafına aktarılmadan önce, AsOrdered nedeni ile orjinal sıra konumlarına yerleştirilir. Bu ek işlem, sonucun elde edilmesini yavaşlatacaktır. Bu nedenle MSND kaynaklarında, AsOrdered genişletme metodunun gerekmedikçe kullanılmaması öğütlenmektedir. Hatta, orderby kullanımının tercih edilmesi önerilmektedir. Yazımızın başında belirttiğimiz Orderby kullanımının neden önemli olduğunu sanırım anlamış bulunuyoruz. Tabiki zorunlu hallerde AsOrdered kullanılmasıda gerekebilir.

Bu yazımda sizlere önemsiz gibi görünen fakat dikkat edilmesi gereken bir konuyu aktarmaya çalıştım. Bir sonraki yazımızda görüşünceye dek mutlu günler dilerim.

Ordering.rar (22,74 kb)

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

PLINQ (Parallel LINQ) - Hello World [Beta 1]

Cuma, 22 Mayıs 2009 07:11 by bsenyurt

Merhaba Arkadaşlar,

Bildiğiniz gibi son yazımı deniz kenarında bir kafede tatildeyken yazmıştım Wink Ama tatil bitti malesef ve tekrardan Morpheus' un sözleri kulaklarımda çınladı "Wellcome to the real world". Cry Yinede 1 haftalığınada olsa tatil yapabildiğime şükrediyorum. Gerçek dünyaya döndükten sonra tabiki bir süre adaptasyon sorunları ile karşılaşıyor insan doğal olaraktan. Bu adaptasyon sorunları içerisinde boğuşurken, neleri araştırabilirim diye düşünürken buluverdim kendimi. Herşeyden önce .Net Framework 4.0 ve Visual Studio 2010 Beta 1 sürümlerinin yayınlandığını hepimiz biliyoruz. Dolayısıyla odaklanılacak konu zaten ap açık ortadaydı. .Net Framework 4.0 içerisinde entegre olarak gelen bir yenilik hemen ilgi odağım oldu. PLINQ(Parallel Language INtegrated Query). Aslında PLINQ yeni çıkmış bir eklenti değil. Zaten uzun süredir .Net Framework 3.5 ve Visual Studio 2008 üzerinde CTP sürümü ile testlerimizi yapabiliyorduk. Ne varki, .Net Framework 4.0 göz önüne alındığında PLINQ ile ilişkili tiplerin System.Core.dll assembly' ının 4.0 versiyonu içerisine doğrudan ilave edildiğini görüyoruz. Aşağıdaki Visual Studio 2010 Object Browser' dan alınan görüntüde bu durum açık bir şekilde gözlemlenebiliyor.

Tabiki öncelikli olarak PLINQ kavramından biraz bahsetmemizde yarar var. PLINQ aslında, Microsfot Research ve CLR(Common Language Runtime) takımları tarafından ortaklaşa geliştirilen Parallel Extensions isimli genişletmelerin sadece bir paçasıdır. Diğer parça ise TBL(Task Parallel Library) dir.(Bunu ilerleyen yazılarımda ele almaya çalışacağım) Her iki yapının kullanım amacı, Yönetimli Kod(Managed Code) tarafındaki eş zamanlı işleyişlerin kolay bir şekilde sağlanmasıdır. Söz konusu yapı PLINQ olunca haliyle, LINQ sorgularının kendi içerisine parçalanarak farklı thread' lerde çalışması ve bu parçaların paralel yürüyerek sonuçların elde edilmesi akla gelmektedir. Gerçektende PLINQ yapısının temel amacı bu şekilde özetlenebilir. Hatta PLINQ için Eş Zamanlı Sorgu Yürütme Motorudur(Concurrency Query Execution Engine) diyebiliriz. PLINQ temel olarak LINQ to XML ve LINQ to Objects gibi uygulama alanları üzerinde etkin bir şekilde kullanılabilmektedir.

NOT : Unutulmaması gereken noktalardan biriside, PLINQ ifadelerinin aslında çift çekirdek ve üstü işlemcilerin yada birden fazla işlemcinin olduğu sistemlerde anlamlı olmasıdır. Nitekim, PLINQ motoru, çalışmakta olan sorgu sürecini, makinenin sahip olduğu çekirdek sayısına göre parçalara ayırır ve yürütür. Bu özellikle büyük çaplı projeler göz önüne alındığında, şirketin sahip olduğu kaç bilgisayar var ise hepsini en azından çift çekirdekli olacak şekilde yenilemek gibi bir maliyet anlamına da gelmemelidir. Nitekim bazı istemci-sunucu mimarilerinde, sunucu tarafında çalışmakta olan pek çok LINQ sorgusu, PLINQ motoru kullanılaraktan daha efektif hale getirilebilir. Bir başka deyişle, istemciler birden fazla çekirdekli işlemcilere sahip olmasalarda, mümkün mertebe LINQ ifadelerini içeren iş  mantıklarının, sunucu tarafında olduğu senaryolarda PLINQ büyük avantajlar sağlayabilir(Çok kısa bir süre önce çalışmakta olduğum bir projede yer alan test makinesinin, 8 işlemcili olduğunu hatırlıyorum Laughing )

Tabi burada var olan nesneler üzerindeki LINQ sorgularının paralel olarak çalıştırılması için, Select, Where gibi genişletme metodlarının(Extension Methods) çalışma sırasında işi farklı parçalara bölebilecek versiyonlarının olması gerektiği düşünülebilir. İşte bu noktada devreye, System.Core assembly' ının 4.0 versiyonu içerisinde yer alan ve System.Linq isim alanında bulunan ParallelEnumerable adlı static sınıf girmektedir.

Bu sınıftaki en önemli genişletme AsParallel isimli fonksiyondur. Bu metodun görevi, IEnumerable türevli bir koleksiyonun paralel olarak sorgulanabilir hale getirilmesi veya hazırlanmasıdır. Öyleki, metod geriye ParallelQuery isimli sınıfa ait bir nesne örneği döndürmektedir. ParallelQuery sınıfı IEnumerable arayüzünü uygulamaktadır ama herşeyden önemlisi paralel sorgulanabilme için gerekli ön hazırlıkları içeren operasyonlarada sahiptir.

Bu teknik detaylar eminimki bir Hello World yazısında sizede sıkıcı gelmiştir. Hiç vakit kaybetmeden basit bir örnek ile ilerlemekte yarar olduğunu düşünmekteyim. Aşağıdaki kod parçası Visual Studio 2010 Beta 1 sürümünde yazılmış basit bir Console uygulamasına aittir.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Product> products = FillProducts();
            Console.WriteLine("Liste dolduruldu. İşlemlere devam etmek için tıklayın");
            Console.ReadLine();

            Stopwatch watch = Stopwatch.StartNew();

            var result1 = from p in products
                          where p.ListPrice >= 10 && p.InStock == true
                          orderby p.Name descending
                          select p;
            Console.WriteLine("Toplam {0} adet ürün bulundu",result1.ToList().Count.ToString());

            Console.WriteLine("Toplam süre {0}",watch.ElapsedMilliseconds.ToString());
            Console.WriteLine("Parallel Olduğunda");

            Stopwatch watch2 = Stopwatch.StartNew();

            var result2 = from p in products.AsParallel()
                          where p.ListPrice >= 10 && p.InStock==true
                          orderby p.Name descending
                          select p;
            Console.WriteLine("Toplam {0} adet ürün bulundu", result2.ToList().Count.ToString());

            Console.WriteLine("Toplam süre {0}", watch2.ElapsedMilliseconds.ToString());
        }

        static List<Product> FillProducts()
        {
            List<Product> products = new List<Product>();

            for (long i = 1; i < 1750000; i++)
            {
                Product prd = new Product {
                    Id = i
                    , Name = "Product" + i.ToString()
                    , ListPrice = i * 0.1M
                    , InStock=i%2==0?true:false
                };
                products.Add(prd);
            }

            return products;
        }
    }

   
    class Product
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public decimal ListPrice { get; set; }
        public bool InStock { get; set; }
    }
}

Uygulama içerisinde Products isimli bir sınıf ve bu tipe ait nesne örneklerinden oluşan bir koleksiyon veri kaynağı olarak kullanılmaktadır. Dikkat edileceği üzere, iki adet LINQ sorgusu bulunmaktadır. Product tipinden olan generic List koleksiyonu, FillProducts metodu yardımıyla tamamen hayali veriler ile doldurulmuştur. Her iki sorguda ListPrice değeri 10' un üzerinde olan ve stokta bulunan ürünleri, adlarına göre ters sırada döndürmektedir. Ancak önemli olan nokta ikinci LINQ ifadesinde AsParallel metodunun kullanılmasıdır. Bu örnek kod parçasını çalıştırdığımda, aşağıdaki ekran görüntüsünde yer alan sonuçları aldım. 

Hemen şunu belirteyim. Programı yazdığım makinede çift çekirdekli Intel işlemci ve 4 Gb Ram bulunmakta. İşletim sistemi olarakta Windows Vista Enterprise yer alıyor. Tabi bu örnek için Intel tabanlı işlemcinin daha büyük önem taşıdığını hemen söyleyebiliriz. Çalışma zamanındanda görüldüğü gibi, paralel olarak yürütülen LINQ ifadesi neredeyse %50 daha az zamanda tamamlanmıştır. (Aslında bu kod parçasını 4 çekirdekli bir işlemcide test etmeyi çok istiyorum. Bu konuda siz değerli okurlarımın yorumlarını ve test sonuçlarını bekliyor olacağım Wink )

Uygulama çalışırken Task Manager aracı ile CPU kullanım durumuna baktığımda ise aşağıdaki sonuçlar ile karşılaştım.

Bu ekran görüntüsünde yer alan sonuçlar tam anlamıyla durmun net analizi olmasada bir parça olsun fikir vermektedir. Yuvarlak içerisine aldığımda kısımlar, sorgunun PLINQ motoru tarafından ele alınmaya başladığı yerlerdeki ölçüm değerleridir. Dikkat edileceği üzere CPU çekirdeklerinin kullanım değerleri %100' e vurmuş durumdadır ki buda aslında, sorgunun çalıştırılması sırasında tüm işlemci gücünün kullanıldığı anlamınada gelmektedir. (Aslında Einstein' ın kuramına göre bu durum göreceli olarak iyi sayılabilir. Ama sayılmayadabilir Undecided )

Yazdığım örnek kod parçasında işlemleri gerçekten yavaşlatmak adına bir sıralama işlemide kullandım. Anacak bunun yapılması zorunlu değildir. Özellikle sıralama kullanılmadığında sorgu çalıştırma sürelerinin birbirlerine çok yakın olduğunu gördüm. Açıkçası, PLINQ' in avantajı gerçekten çok uzun sürebilecek sorgular söz konusu olduğunda ortaya çıkmata. Bu nedenle her LINQ sorgusunun PLINQ formatına dönüştürülmesininde anlamlı olmadığını(olmayacağını) söyleyebiliriz. Nitekim, bazı durumlarda herşey tersine dönebilir. Örneğin aşağıdaki kod parçasını göz önüne alalım.

int[] values = new int[100];Random rnd = new Random();
for (int i = 1; i < values.Length; i++)
{
 values[i] = rnd.Next(1, 1000);
}

Stopwatch watch3 = Stopwatch.StartNew();
var result3 = from value in values
              where value % 2 == 0
              select value;
Console.WriteLine("Toplam {0} çift sayı var",result3.ToList().Count.ToString());
Console.WriteLine(watch3.ElapsedMilliseconds.ToString());

Stopwatch watch4 = Stopwatch.StartNew();
var result4 = from value in values.AsParallel()
              where value % 2 == 0
              select value;
Console.WriteLine("Toplam {0} çift sayı var", result4.ToList().Count.ToString());
Console.WriteLine(watch4.ElapsedMilliseconds.ToString());

Bu kod parçasındaki LINQ sorgularında, 100 tane raslantısal ve 1 ile 1000 arasında olan tamsayı değerlerinden oluşan bir dizi içerisinde kaç çift sayı olduğu tespit edilmektedir. İkinci sorgu, PLINQ motoru tarafından ele alınmaktadır. PLINQ' in, sorguyu paralel olan iş parçalarına bölerek çalıştırdığı düşünüldüğünde, ikinci ifadenin birincisine göre çok daha hızlı çalışması gerektiği tahmin edilebilir. Ama gerçekten böylemi olacaktır. İşte sonuçlar...

Aslında bu sonuç son derece doğaldır. PLINQ motoru çalışma zamanında, çekirdeklere bölünecek işler için hazırlıklar yapmalıdır, thread' leri ayarlamalıdır vb... Bu ön hazırlıklar nedeni ile zaten sorgunun sürece girmesi başlı başına bir zaman kaybı anlamına gelmektedir. Bu örnek en basit anlamda, PLINQ' in her LINQ ifadesi için ele alınmaması gerektiğinide göstermektedir.

Böylece geldik bir yazımızın daha sonuna. Tatil dönüşü sonrası üzerimdeki adaptasyon bıkkınlığını hafifleten bu yazımda sizlere, .Net Framework 4.0 içerisinde artık standart olarak yer alan ve Parallel Extension mimarisinin bir parçası olan PLINQ konusunu anlatmaya çalıştım. Elbetteki PLINQ içerisindede çok daha fazlası var. Bunlarıda ilerleyen yazılarımda aktarmaya çalışıyor olacağım. Sizlerde bu adresten Parallel Extension ile ilişkili son bilgileri alabilirsiniz. Hatta şu saatlerde VS 2010 ile gelen yeniliklerde anlatılmakta. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

HelloWorld.rar (21,81 kb)

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