PLINQ ile olası tuzaklar

Çoğu durumda PLINQ, sıralı LINQ to Objects sorgularına göre önemli performans geliştirmeleri sağlayabilir. Ancak, sorgu yürütmesini paralelleştirme çalışması, sıralı kodda o kadar yaygın olmayan veya hiç karşılaşmayan sorunlara yol açabilecek karmaşıklığa neden olabilir. Bu konuda, PLINQ sorguları yazarken kaçınılması gereken bazı yöntemler listelenir.

Paralelin her zaman daha hızlı olduğunu varsaymayın

Paralelleştirme bazen PLINQ sorgularının LINQ to Objects eşdeğerinden daha yavaş çalışmasına neden olur. Temel kural, birkaç kaynak öğeye ve hızlı kullanıcı temsilcilerine sahip sorguların çok fazla hızlanma olasılığının düşük olmasıdır. Ancak, performansa birçok faktör dahil olduğundan, PLINQ kullanıp kullanmamaya karar vermeden önce gerçek sonuçları ölçmenizi öneririz. Daha fazla bilgi için bkz. PLINQ'de Hızlandırmayı Anlama .

Paylaşılan bellek konumlarına yazmaktan kaçının

Sıralı kodda, statik değişkenlerden veya sınıf alanlarından okuma veya yazma işlemi sık karşılaşılan bir durum değildir. Ancak, bu tür değişkenlere aynı anda birden çok iş parçacığı eriştiğinde, yarış koşullarının oluşma olasılığı yüksektir. Değişkene erişimi eşitlemek için kilitleri kullanabilirsiniz, ancak eşitlemenin maliyeti performansı etkileyebilir. Bu nedenle, PLINQ sorgusunda paylaşılan duruma erişimi mümkün olduğunca engellemenizi veya en azından sınırlamanızı öneririz.

Örnek: Paylaşılan bellekli yarış durumu

Aşağıdaki örnekte, birden çok thread paylaşılan bir değişkene yazdığında oluşan bir yarış koşulu gösterilmektedir. Değişkene total eşitlenmeden birden çok iş parçacığı tarafından eşzamanlı olarak erişilir ve değiştirilir ve bu da öngörülemeyen sonuçlara yol açar:

static void DemonstrateRaceCondition()
{
    int total = 0;
    var numbers = Enumerable.Range(0, 10000);

    // UNSAFE: Multiple threads writing to shared variable
    numbers.AsParallel().ForAll(n => total += n);

    Console.WriteLine($"Total (with race condition): {total}");
    // Expected: 49,995,000 but result is unpredictable due to race condition
}
Shared Sub DemonstrateRaceCondition()
    Dim total As Integer = 0
    Dim numbers = Enumerable.Range(0, 10000)

    ' UNSAFE: Multiple threads writing to shared variable
    numbers.AsParallel().ForAll(Sub(n) total += n)

    Console.WriteLine($"Total (with race condition): {total}")
    ' Expected: 49,995,000 but result is unpredictable due to race condition
End Sub

Bu kodda işlem total += n atomik değildir. Mevcut total değerinin okunması, n eklenmesi ve sonucun total öğesine geri yazılması. Bu işlemi aynı anda yürüten birden çok iş parçacığı aynı değeri okuyabilir, farklı iş parçacıklarında ekleyebilir ve birbirinin üzerine yazan sonuçları geri yazabilir. Bu, bazı eklemelerin kaybedilmesine neden olur ve hatalı bir nihai sonuç oluşturur.

Doğru yaklaşım, paylaşılan değiştirilebilir durum gerektirmeyen iş parçacığı güvenli işlemleri kullanmaktır:

static void DemonstrateCorrectApproach()
{
    var numbers = Enumerable.Range(0, 10000);

    // SAFE: Use thread-safe aggregate operation
    int total = numbers.AsParallel().Sum();

    Console.WriteLine($"Total (correct): {total}");
    // Result is always 49,995,000
}
Shared Sub DemonstrateCorrectApproach()
    Dim numbers = Enumerable.Range(0, 10000)

    ' SAFE: Use thread-safe aggregate operation
    Dim total As Integer = numbers.AsParallel().Sum()

    Console.WriteLine($"Total (correct): {total}")
    ' Result is always 49,995,000
End Sub

Yöntem, paralelleştirmeyi iş parçacığı güvenliğini koruyarak dahili olarak işler ve açık eşitlemeye gerek kalmadan doğru sonuçlar sağlar. Diğer güvenli yaklaşımlar arasında, özel toplamalar için Aggregate kullanmak veya ConcurrentBag<T> gibi iş parçacığı güvenli koleksiyonlarda sonuçları toplamak bulunmaktadır.

Fazla paralelleştirmeden kaçının

AsParallel yöntemini kullanarak kaynak koleksiyonunu bölümlemenin ve çalışan iş parçacıklarını eşitlemenin ek yük maliyetlerine katlanırsınız. Paralelleştirmenin avantajları, bilgisayardaki işlemci sayısıyla daha da sınırlıdır. Yalnızca bir işlemcide birden çok işlem ilişkili iş parçacığı çalıştırılarak kazanılacak bir hız yoktur. Bu nedenle, bir sorguyu fazla paralelleştirmemeye dikkat etmeniz gerekir.

Aşırı paralelleştirmenin gerçekleşebildiği en yaygın senaryo, aşağıdaki kod parçacığında gösterildiği gibi iç içe geçmiş sorgulardadır.

var q = from cust in customers.AsParallel()
        from order in cust.Orders.AsParallel()
        where order.OrderDate > date
        select new { cust, order };
Dim q = From cust In customers.AsParallel()
        From order In cust.Orders.AsParallel()
        Where order.OrderDate > aDate
        Select New With {cust, order}

Bu durumda, aşağıdaki koşullardan biri veya daha fazlası geçerli olmadıkça yalnızca dış veri kaynağını (müşteriler) paralel hale getirmek en iyisidir:

  • Dahili veri kaynağı (cust.Orders) çok uzun olduğu bilinir.

  • Her siparişte pahalı bir hesaplama gerçekleştiriyorsunuz. (Örnekte gösterilen işlem pahalı değildir.)

  • Hedef sistemin üzerinde sorgu cust.Ordersparalelleştirilerek oluşturulacak iş parçacığı sayısını işlemek için yeterli işlemciye sahip olduğu bilinmektedir.

Her durumda, en uygun sorgu şeklini belirlemenin en iyi yolu test etmek ve ölçmektir. Daha fazla bilgi için bkz . Nasıl yapılır: PLINQ Sorgu Performansını Ölçme.

İş parçacığı güvenli olmayan yöntemlere yapılan çağrılardan kaçının

Bir PLINQ sorgusundan iş parçacığı güvenli olmayan örnek yöntemlerine yazmak, programınızda algılanabilir veya algılanamayacak veri bozulmasına neden olabilir. Ayrıca özel durumlara yol açabilir. Aşağıdaki örnekte, sınıf tarafından desteklenmeyen FileStream.Write yöntemini aynı anda birden çok iş parçacığı çağırmaya çalışacaktır.

Dim fs As FileStream = File.OpenWrite(…)
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));

İş parçacığı güvenli yöntemlere yapılan çağrıları sınırla

.NET statik yöntemlerin çoğu iş parçacığı açısından güvenlidir ve aynı anda birden çok iş parçacığından çağrılabilir. Ancak bu durumlarda bile, söz konusu eşitleme sorguda önemli bir yavaşlamayla sonuçlanabilir.

Uyarı

Bunu kendiniz test etmek için sorgularınıza WriteLine çağrıları ekleyebilirsiniz. Bu yöntem, belge örneklerinde tanıtım amacıyla kullanılsa da PLINQ sorgularında kullanmayın.

Gereksiz sıralama işlemlerinden kaçının

PLINQ bir sorguyu paralel olarak yürüttüğünde, kaynak diziyi birden çok iş parçacığında eşzamanlı olarak çalıştırılabilir bölümlere böler. Varsayılan olarak, bölümlerin işlenme ve sonuçların teslim edilme sırası tahmin edilebilir değildir (gibi OrderByişleçler hariç). PLINQ'a herhangi bir kaynak dizisinin sırasını korumasını sağlayabilirsiniz, ancak bunun performans üzerinde olumsuz bir etkisi vardır. Mümkün olduğunda en iyi yöntem, sorguları düzen korumasına güvenmemeleri için yapılandırmaktır. Daha fazla bilgi için bkz. PLINQ'te Sipariş Koruması.

Mümkün olduğunda ForEach için ForAll'ı tercih edin

PLINQ bir sorguyu birden çok iş parçacığında yürütse de, sonuçları bir foreach döngüsünde (Visual Basic For Each) tüketirseniz, sorgu sonuçları bir iş parçacığında yeniden birleştirilmeli ve numaralandırıcı tarafından seri olarak erişilmelidir. Bazı durumlarda kaçınılmazdır, ancak mümkün olduğunda her iş parçacığının kendi sonuçlarını yazabilmesi için ForAll yöntemini kullanın; örneğin, System.Collections.Concurrent.ConcurrentBag<T> gibi iş parçacığı güvenli bir koleksiyona yazarak.

Aynı sorun için Parallel.ForEachde geçerlidir. Başka bir deyişle, source.AsParallel().Where().ForAll(...) kesinlikle Parallel.ForEach(source.AsParallel().Where(), ...) yerine tercih edilmelidir.

İş parçacığı bağlılık sorunlarına dikkat edin

Örneğin, Single-Threaded Apartment (STA) bileşenleri, Windows Forms ve Windows Presentation Foundation (WPF) için COM birlikte çalışabilirlik gibi bazı teknolojiler, kodun belirli bir iş parçacığında çalışmasını gerektiren iş parçacığı bağlılığı kısıtlamalarını uygular. Örneğin, hem Windows Forms hem de WPF denetime yalnızca oluşturulduğu iş parçacığında erişilebilir. PLINQ sorgusunda bir Windows Forms denetiminin paylaşılan durumuna erişmeye çalışırsanız, hata ayıklayıcı çalıştırılıyorsa bir istisna fırlatılır. (Bu ayar kapatılabilir.) Ancak, sorgunuz UI iş parçacığında tüketiliyorsa, bu kod yalnızca bir iş parçacığında yürütüldüğü için sorgu sonuçlarını numaralandıran döngüden kontrole erişebilirsiniz.

ForEach, For ve ForAll yinelemelerinin her zaman paralel olarak yürütüldüğünü varsaymayın

Parallel.For, Parallel.ForEach veya ForAll döngüsündeki bireysel yinelemelerin paralel olarak yürütülmesi mümkün olmakla birlikte, zorunlu değildir; bunu akılda tutmak önemlidir. Bu nedenle, yinelemelerin paralel yürütülmesine veya yinelemelerin belirli bir sırada yürütülmesine doğruluğa bağlı olan herhangi bir kod yazmaktan kaçınmanız gerekir.

Örneğin, bu kodun kilitlenme olasılığı yüksektir:

Dim mre = New ManualResetEventSlim()
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll(Sub(j)
   If j = Environment.ProcessorCount Then
       Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
       mre.Set()
   Else
       Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
       mre.Wait()
   End If
End Sub) ' deadlocks
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll((j) =>
{
    if (j == Environment.ProcessorCount)
    {
        Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
        mre.Set();
    }
    else
    {
        Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
        mre.Wait();
    }
}); //deadlocks

Bu örnekte, bir yineleme bir olayı ayarlar ve diğer tüm yinelemeler olay üzerinde bekler. Olay ayarı yinelemesi tamamlanana kadar bekleyen yinelemelerin hiçbiri tamamlanamaz. Ancak, olay tetikleme yinelemesi henüz yürütülme şansı bulamadan önce, bekleyen yinelemelerin, paralel döngüyü yürütmekte kullanılan tüm iş parçacıklarını durdurması mümkündür. Bu bir çıkmaza neden olur; olay oluşturma yinelemesi hiçbir zaman yürütülmeyecek ve bekleyen yinelemeler hiçbir zaman uyanmayacaktır.

Özellikle, paralel döngünün bir yinelemesi hiçbir zaman ilerleme kaydetmek için döngünün başka bir yinelemesini beklememelidir. Paralel döngü yinelemeleri sıralı olarak ancak ters sırada zamanlamaya karar verirse bir kilitlenme oluşur.

Ayrıca bakınız