Udostępnij za pomocą


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ą niewiele elementów źródłowych i szybkich delegatów użytkownika, są mało prawdopodobne, aby znacznie ulec przyspieszeniu. 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 Zrozumienie przyspieszenia w PLINQ (Understanding Speedup in PLINQ).

Unikaj zapisywania w lokalizacjach pamięci udostępnionej

W kodzie sekwencyjnym nie jest niczym niezwykłym odczytywanie ze zmiennych statycznych lub pól klas ani zapisywanie do nich. 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 nadmiernego równoległego 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łość, ma miejsce w zapytaniach zagnieżdżonych, co pokazano w poniższym fragmencie.

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 tym przypadku najlepiej jest zrównoleglać tylko zewnętrzne źródło danych (klienci), chyba że jeden lub więcej z poniższych warunków jest spełnione:

  • Wewnętrzne źródło danych (cust.Zamówienia) jest znane jako bardzo długie.

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

  • Wiadomo, że system docelowy ma wystarczającą liczbę procesorów do obsługi liczby wątków, które zostaną wygenerowane przez zrównoleglenie zapytania na 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 niebezpiecznych dla wątków

Zapisywanie w niebezpiecznych wątkowo metodach instancji z zapytania PLINQ może prowadzić do uszkodzenia danych, które mogą pozostać niewykryte 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 dla wątków

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 / Notatka

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 od 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), to wyniki zapytania muszą być scalone z powrotem do jednego wątku i przetwarzane seryjnie przez enumerator. W niektórych przypadkach jest to nieuniknione; jednak zawsze, gdy to możliwe, użyj metody ForAll aby umożliwić każdemu wątkowi wyprowadzenie własnych wyników, przykładowo poprzez zapis w bezpiecznej wątkowo kolekcji, takiej jak System.Collections.Concurrent.ConcurrentBag<T>.

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

Należy pamiętać o problemach z przynależnością wątków

Niektóre technologie, na przykład współdziałanie z modelem COM dla składników Single-Threaded Apartment (STA), Windows Forms i Windows Presentation Foundation (WPF), nakładają ograniczenia dotyczące powiązania wątku, które wymagają, aby kod był wykonywany na 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 udostępnionego stanu kontrolki Windows Forms w zapytaniu PLINQ, zgłoszony zostanie wyjątek, jeśli uruchamiasz w debugerze. (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 iteracje w Parallel.Forpętli, Parallel.ForEachlub ForAll mogą, 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 prawdopodobnie spowoduje zakleszczenie:

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 się zakończyć, dopóki iteracja ustawiająca zdarzenia nie zostanie zakończona. Istnieje jednak możliwość, że iteracje oczekujące blokują wszystkie wątki używane do wykonywania pętli równoległej, zanim iteracja ustawiająca zdarzenia zdążyła się wykonać. Spowoduje to zakleszczenie — iteracja ustawiania zdarzenia nigdy nie zostanie wykonana, a oczekujące iteracje nigdy się nie obudzą.

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 także