Aracılığıyla paylaş


Veri ve Görev Paralelliğinde Olası Tuzaklar

Çoğu durumda Parallel.For ve Parallel.ForEach sıradan sıralı döngüler üzerinde önemli performans geliştirmeleri sağlayabilir. Ancak, döngünün paralelleştirilmesi çalışması, sıralı kodda o kadar yaygın olmayan veya hiç karşılaşmayan sorunlara yol açabilecek karmaşıklığı ortaya çıkarır. Bu konuda, paralel döngüler yazarken kaçınılması gereken bazı uygulamalar listelenir.

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

Bazı durumlarda paralel döngü sıralı eşdeğerinden daha yavaş çalışabilir. Temel kural, az sayıda yinelemesi olan paralel döngülerin ve hızlı kullanıcı temsilcilerinin çok fazla hızlanma olasılığının düşük olmasıdır. Ancak, performansa birçok faktör dahil olduğundan, her zaman gerçek sonuçları ölçmenizi öneririz.

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, mümkün olduğunca paralel bir döngüde paylaşılan duruma erişimi önlemenizi veya en azından sınırlamanızı öneririz. Bunu yapmak için en iyi yol, döngü yürütme sırasında iş parçacığı yerel durumunu depolamak üzere Parallel.For değişkeni kullanan Parallel.ForEach ve System.Threading.ThreadLocal<T> aşırı yüklemelerini kullanmaktır. Daha fazla bilgi için bkz Nasıl yapılır: Paralel.For Döngüsünü İş Parçacığı-Özel Değişkenlerle Yazma ve Nasıl yapılır: Paralel.ForEach Döngüsünü Bölüm-Özel Değişkenlerle Yazma.

Aşırı Paralelleştirmeden Kaçının

Paralel döngüler kullanmak, kaynak koleksiyonunu bölmenin ve çalışma iş parçacıklarının eşzamanlanmasının fazladan maliyetlerine neden olur. 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 döngünün aşırı paralel hale getirilmemesi için dikkatli olmanız gerekir.

Fazla paralelleştirmenin gerçekleşebileceği en yaygın senaryo iç içe geçmiş döngülerdedir. Çoğu durumda, aşağıdaki koşullardan biri veya daha fazlası geçerli olmadıkça yalnızca dış döngünün paralelleştirilmesi en iyisidir:

  • İç döngünün çok uzun olduğu bilinmektedir.

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

  • Hedef sistemin, işleme paralelleş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.

İş parçacığı güvenliği olmayan yöntemlere çağrı yapmaktan kaçının.

Paralel döngüden iş parçacığı güvenli olmayan örnek metotlarına yazmak, programınızda algılanan veya algılanmayan veri bozulmasına neden olabilir. Ayrıca özel durumlara yol açabilir. Aşağıdaki örnekte, sınıf tarafından desteklenmeyen FileStream.WriteByte yöntemini aynı anda birden çok iş parçacığı çağırmaya çalışacaktır.

FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));
Dim fs As FileStream = File.OpenWrite(filepath)
Dim bytes() As Byte
ReDim bytes(1000000)
' ...init byte array
Parallel.For(0, bytes.Length, Sub(n) fs.WriteByte(bytes(n)))

İş Parçacığı Güvenli Yöntemlere Çağrıları Sınırlayın

.NET'teki çoğu statik yöntem 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.

Not

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, gerekli olmadıkça paralel döngülerde kullanmayın.

İş Parçacığı Bağlılığı Sorunlarına Dikkat Edin

Tek İş Parçacıklı Daire (STA) bileşenleri, Windows Forms ve Windows Presentation Foundation (WPF) için COM birlikte çalışabilirliği gibi bazı teknolojiler, kodun belirli bir iş parçacığında çalıştırılmasını gerektiren iş parçacığı benşimi kısıtlamaları uygular. Örneğin, hem Windows Forms hem de WPF'de denetime yalnızca oluşturulduğu iş parçacığında erişilebilir. Bu, örneğin, iş parçacığı zamanlayıcısını yalnızca ui iş parçacığında çalışmayı zamanlamak üzere yapılandırmadığınız sürece paralel döngüden bir liste denetimini güncelleştiremeyeceğiniz anlamına gelir. Daha fazla bilgi için bkz. Eşitleme bağlamı belirtme.

Parallel.Invoke Tarafından Çağrılan Temsilcilerde Beklerken Dikkatli Olun

Belirli durumlarda, Görev Paralel Kitaplığı bir görevi satır içinde sıralar ve bu da o anda yürütülen iş parçacığında görev üzerinde çalıştığı anlamına gelir. (Daha fazla bilgi için bkz. Görev Zamanlayıcıları.) Bu performans iyileştirmesi bazı durumlarda kilitlenmeye yol açabilir. Örneğin, iki görev aynı temsilci kodunu çalıştırabilir ve bu kod bir olay gerçekleştiğinde sinyal verir ve diğer görevin sinyal göndermesini bekler. İkinci görev, ilk görevle aynı iş parçacığında satırlanmışsa ve ilki Bekleme durumuna geçerse, ikinci görev hiçbir zaman olayını sinyal veremeyecektir. Böyle bir durumla karşılaşmamak için Bekleme işleminde bir zaman aşımı belirtebilir veya bir görevin diğerini engelleyememesine yardımcı olmak için açık iş parçacığı oluşturucuları kullanabilirsiniz.

ForEach, For ve ForAll Yinelemelerinin Her Zaman Paralel Olarak Yürütüldüğünü Varsaymayın

Unutulmamalıdır ki, For, ForEach veya ForAll döngüsündeki tek tek yinelemelerin paralel olarak yürütülmesi gerekebilir, ancak şart değildir. 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:

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

UI İş Parçacığında Paralel Döngüler Yürütmekten Kaçının

Uygulamanızın kullanıcı arabirimini (UI) duyarlı tutmak önemlidir. Bir işlem paralelleştirmeyi garanti edecek kadar çalışma içeriyorsa, büyük olasılıkla ui iş parçacığında çalıştırılmamalıdır. Bunun yerine, bir arka plan iş parçacığında çalıştırılacak işlemi boşaltması gerekir. Örneğin, bir kullanıcı arabirimi denetimine işlenmesi gereken bazı verileri hesaplamak için paralel döngü kullanmak istiyorsanız, döngünün doğrudan ui olay işleyicisinde değil, görev örneğinde yürütülmesini düşünmelisiniz. Yalnızca çekirdek hesaplama tamamlandığında kullanıcı arabirimi güncelleştirmesini ui iş parçacığına geri hazırlamanız gerekir.

UI iş parçacığında paralel döngüler çalıştırırsanız, döngü içinden kullanıcı arabirimi denetimlerinin güncelleştirilmesini önlemeye dikkat edin. Kullanıcı arabirimi iş parçacığında yürütülen paralel bir döngüden kullanıcı arabirimi denetimlerini güncelleştirmeye çalışmak, kullanıcı arabirimi güncelleştirmesinin nasıl çağrıldığına bağlı olarak durum bozulmasına, özel durumlara, gecikmeli güncelleştirmelere ve hatta kilitlenmelere neden olabilir. Aşağıdaki örnekte paralel döngü, tüm yinelemeler tamamlanana kadar üzerinde yürütülmekte olduğu ui iş parçacığını engeller. Ancak, döngünün bir yinelemesi bir arka plan iş parçacığında çalışıyorsa (For yapabileceği gibi), Invoke çağrısı bir iletinin UI iş parçacığına gönderilmesine ve bu iletinin işlenmesini beklerken engellenmesine neden olur. UI iş parçacığı, For çalıştırıldığından bloke olur, bu yüzden iletiler asla işlenemez ve UI iş parçacığı kilitlenir.

private void button1_Click(object sender, EventArgs e)
{
    Parallel.For(0, N, i =>
    {
        // do work for i
        button1.Invoke((Action)delegate { DisplayProgress(i); });
    });
}
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Dim iterations As Integer = 20
    Parallel.For(0, iterations, Sub(x)
                                    Button1.Invoke(Sub()
                                                       DisplayProgress(x)
                                                   End Sub)
                                End Sub)
End Sub

Aşağıdaki örnek, döngüyü bir görev örneğinde çalıştırarak kilitlenmenin nasıl önleneceğini göstermektedir. UI iş parçacığı döngü tarafından engellenmez ve ileti işlenebilir.

private void button2_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew(() =>
        Parallel.For(0, N, i =>
        {
            // do work for i
            button1.Invoke((Action)delegate { DisplayProgress(i); });
        })
         );
}
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Dim iterations As Integer = 20
    Task.Factory.StartNew(Sub() Parallel.For(0, iterations, Sub(x)
                                                                Button1.Invoke(Sub()
                                                                                   DisplayProgress(x)
                                                                               End Sub)
                                                            End Sub))
End Sub

Ayrıca bkz.