Udostępnij za pośrednictwem


Potencjalne pułapki z PLINQ

W wielu przypadkach plINQ może zapewnić znaczną poprawę wydajności w przypadku sekwencyjnych zapytań LINQ to Objects. Jednak praca zrównoleglizowania wykonywania zapytania wprowadza złożoność, która może prowadzić do problemów, które w kodzie sekwencyjnym nie są tak powszechne lub w ogóle nie występują. W tym temacie wymieniono niektóre rozwiązania, które należy unikać podczas pisania zapytań PLINQ.

Nie zakładaj, że równoległe działanie jest zawsze szybsze

Równoległość czasami powoduje, że zapytanie PLINQ działa wolniej niż jego odpowiednik LINQ to Objects. Podstawową regułą jest to, że zapytania, które mają kilka elementów źródłowych i szybkich delegatów użytkownika, są mało prawdopodobne, aby przyspieszyć dużo. Jednak ze względu na to, że wiele czynników jest zaangażowanych w wydajność, zalecamy mierzenie rzeczywistych wyników przed podjęciem decyzji, czy używać PLINQ. Aby uzyskać więcej informacji, zobacz Understanding Speedup in PLINQ (Opis szybkości w PLINQ).

Unikaj zapisywania w lokalizacjach pamięci udostępnionej

W kodzie sekwencyjnym nie jest rzadko odczytywane ze zmiennych statycznych lub pól klas ani zapisywać ich na ich podstawie. Jednak za każdym razem, gdy wiele wątków uzyskuje dostęp do takich zmiennych jednocześnie, istnieje duży potencjał warunków wyścigu. Mimo że można używać blokad do synchronizowania dostępu ze zmienną, koszt synchronizacji może zaszkodzić wydajności. W związku z tym zalecamy, aby uniknąć dostępu do stanu udostępnionego w zapytaniu PLINQ lub przynajmniej go ograniczyć, jak to możliwe.

Unikaj nadmiernej równoległej przetwarzania

Korzystając z AsParallel metody, ponosisz koszty związane z partycjonowaniem kolekcji źródłowej i synchronizowaniem wątków roboczych. Korzyści z równoległości są dodatkowo ograniczone przez liczbę procesorów na komputerze. Nie można przyspieszyć, uruchamiając wiele wątków powiązanych z obliczeniami tylko na jednym procesorze. W związku z tym należy zachować ostrożność, aby nie wykonywać zbyt równoległych zapytań.

Najbardziej typowym scenariuszem, w którym może wystąpić nadmierna równoległa przetwarzanie, jest w zapytaniach zagnieżdżonych, jak pokazano w poniższym fragmencie kodu.

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}

W takim przypadku najlepiej jest zrównać tylko zewnętrzne źródło danych (klientów), chyba że obowiązują co najmniej jedno z następujących warunków:

  • Wewnętrzne źródło danych (cust). Zamówienia) są znane bardzo długo.

  • Wykonujesz kosztowne obliczenia dla każdego zamówienia. (Operacja pokazana w przykładzie nie jest kosztowna).

  • System docelowy jest znany ze wystarczającej liczby procesorów do obsługi liczby wątków, które zostaną wygenerowane przez równoległe wykonywanie zapytania w systemie cust.Orders.

We wszystkich przypadkach najlepszym sposobem określenia optymalnego kształtu zapytania jest przetestowanie i pomiar. Aby uzyskać więcej informacji, zobacz How to: Measure PLINQ Query Performance (Jak mierzyć wydajność zapytań PLINQ).

Unikaj wywołań metod niezwiązanych z wątkami

Zapisywanie w metodach wystąpienia niezwiązanych z wątkami z zapytania PLINQ może prowadzić do uszkodzenia danych, które mogą nie zostać wykryte w programie. Może również prowadzić do wyjątków. W poniższym przykładzie wiele wątków próbuje wywołać metodę FileStream.Write jednocześnie, która nie jest obsługiwana przez klasę.

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));

Ogranicz wywołania metod bezpiecznych wątkowo

Większość metod statycznych na platformie .NET jest bezpieczna wątkowo i może być wywoływana z wielu wątków jednocześnie. Jednak nawet w takich przypadkach wykonywana synchronizacja może prowadzić do znacznego spowolnienia w zapytaniu.

Uwaga

Możesz to przetestować samodzielnie, wstawiając kilka wywołań do WriteLine w zapytaniach. Mimo że ta metoda jest używana w przykładach dokumentacji do celów demonstracyjnych, nie należy jej używać w zapytaniach PLINQ.

Unikaj niepotrzebnych operacji porządkowania

Gdy PLINQ wykonuje zapytanie równolegle, dzieli sekwencję źródłową na partycje, które mogą być obsługiwane jednocześnie w wielu wątkach. Domyślnie kolejność przetwarzania partycji i dostarczanie wyników nie jest przewidywalne (z wyjątkiem operatorów takich jak OrderBy). Można poinstruować PLINQ, aby zachować kolejność dowolnej sekwencji źródłowej, ale ma to negatywny wpływ na wydajność. Najlepszym rozwiązaniem, jeśli to możliwe, jest struktura zapytań, aby nie polegały na zachowaniu kolejności. Aby uzyskać więcej informacji, zobacz Zachowywanie kolejności w PLINQ.

Preferuj forAll foreach, gdy jest to możliwe

Mimo że PLINQ wykonuje zapytanie w wielu wątkach, jeśli używasz wyników w foreach pętli (For Each w Visual Basic), wyniki zapytania muszą zostać scalone z powrotem do jednego wątku i uzyskiwane dostęp seryjny przez moduł wyliczający. W niektórych przypadkach jest to nieuniknione; jednak zawsze, gdy to możliwe, użyj ForAll metody , aby umożliwić każdemu wątkowi wyprowadzenie własnych wyników, na przykład poprzez zapisanie w kolekcji bezpiecznej wątkowo, takiej jak System.Collections.Concurrent.ConcurrentBag<T>.

Ten sam problem dotyczy .Parallel.ForEach Innymi słowy, source.AsParallel().Where().ForAll(...) powinno być zdecydowanie preferowane.Parallel.ForEach(source.AsParallel().Where(), ...)

Należy pamiętać o problemach z koligacją wątków

Niektóre technologie, na przykład współdziałanie modelu COM dla składników typu Single-Threaded Apartment (STA), Windows Forms i Windows Presentation Foundation (WPF), nakładają ograniczenia koligacji wątków, które wymagają uruchomienia kodu w określonym wątku. Na przykład w formularzach Windows Forms i WPF dostęp do kontrolki można uzyskać tylko w wątku, w którym został utworzony. Jeśli spróbujesz uzyskać dostęp do stanu udostępnionego kontrolki Windows Forms w zapytaniu PLINQ, w przypadku działania debugera zostanie zgłoszony wyjątek. (To ustawienie można wyłączyć). Jeśli jednak zapytanie jest używane w wątku interfejsu użytkownika, możesz uzyskać dostęp do kontrolki z foreach pętli, która wylicza wyniki zapytania, ponieważ ten kod jest wykonywany tylko w jednym wątku.

Nie zakładaj, że iteracji programu ForEach, For i ForAll zawsze są wykonywane równolegle

Należy pamiętać, że poszczególne iteracji w Parallel.Forpętli , Parallel.ForEachlub ForAll mogą być wykonywane równolegle, ale nie muszą być wykonywane równolegle. Dlatego należy unikać pisania kodu, który zależy od poprawności równoległego wykonywania iteracji lub wykonywania iteracji w dowolnej kolejności.

Na przykład ten kod może zakleszczeć:

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

W tym przykładzie jedna iteracja ustawia zdarzenie, a wszystkie inne iteracji czekają na zdarzenie. Żadna z iteracji oczekujących nie może zakończyć się do momentu ukończenia iteracji ustawienia zdarzeń. Istnieje jednak możliwość, że iteracje oczekujące blokują wszystkie wątki używane do wykonywania pętli równoległej, zanim iteracja ustawienia zdarzeń miała szansę wykonać. Spowoduje to zakleszczenie — iteracja ustawienia zdarzeń nigdy nie zostanie wykonana, a oczekujące iteracji nigdy się nie obudzi.

W szczególności jedna iteracja pętli równoległej nigdy nie powinna czekać na inną iterację pętli, aby poczynić postępy. Jeśli pętla równoległa zdecyduje się zaplanować iteracje sekwencyjnie, ale w odwrotnej kolejności, wystąpi impas.

Zobacz też