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ı için büyük bir potansiyel vardır. 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 için bir System.Threading.ThreadLocal<T> değişken kullanan ve Parallel.ForEach aşırı yüklemelerini Parallel.For kullanmaktır. Daha fazla bilgi için bkz . Nasıl yapılır: İş Parçacığı Yerel Değişkenleri ile Parallel.For Döngüsü Yazma ve Nasıl yapılır: Partition-Local Değişkenleriyle Parallel.ForEach Döngüsü Yazma.
Aşırı Paralelleştirmeden Kaçının
Paralel döngüleri kullanarak kaynak koleksiyonu bölümleme ve çalışan iş parçacıklarını eşitlemenin ek yük maliyetlerine neden olursunuz. 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ştirebilirsiniz. (Örnekte gösterilen işlem pahalı değildir.)
Hedef sistemin üzerinde sorgu
cust.Orders
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ığı Olmayan Kasa Yöntemlerine Çağrılardan Kaçınma
Paralel döngüden iş parçacığı güvenli olmayan örnek yöntemlerine yazmak, programınızda algılanmayan 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 birden çok iş parçacığı aynı anda yöntemini çağırmaya çalışı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ığı Kasa Yöntemlerine Çağrıları Sınırlama
.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
Sorgularınıza bazı çağrılar WriteLine ekleyerek bunu kendiniz test edebilirsiniz. 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ığı Benzşimi 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 çiziliyse ve ilki Bekleme durumuna geçerse, ikinci görev hiçbir zaman olayına 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
veya döngüsündeki ForForEachForAll tek tek yinelemelerin paralel olarak yürütülmesi gerekmeyebilir. 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 {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
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 tamamlanamadı. Ancak, olay ayarı yinelemesinin yürütme şansı olmadan önce, bekleyen yinelemelerin paralel döngü yürütmek için kullanılan tüm iş parçacıklarını engellemesi mümkündür. Bu bir kilitlenmeye neden olur; olay ayarı 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 yinelemesi bir arka plan iş parçacığında çalışıyorsa (olduğu gibi For ), Invoke çağrısı bir iletinin ui iş parçacığına gönderilmesine neden olur ve bu iletinin işlenmesini beklerken engeller. kullanıcı arabirimi iş parçacığı tarafından çalıştırıldığından Forileti hiçbir zaman işlenemez ve UI iş parçacığı kilitlenmesi oluşur.
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 örnekte, döngü bir görev örneğinde çalıştırılarak kilitlenmenin nasıl önlediği gösterilmektedir. UI iş parçacığı döngü tarafından engellenmez ve ileti işlenebilir.
private void button1_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