Udostępnij za pośrednictwem


Potencjalne pułapki związane z równoległością danych i zadań

W wielu przypadkach Parallel.For i Parallel.ForEach może zapewnić znaczną poprawę wydajności w przypadku zwykłych pętli sekwencyjnych. Jednak praca równoległości pętli 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 pętli równoległych.

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

W niektórych przypadkach pętla równoległa może działać wolniej niż jego sekwencyjny odpowiednik. Podstawową regułą jest to, że pętle równoległe, które mają niewiele iteracji i szybkich delegatów użytkownika, jest mało prawdopodobne, aby przyspieszyć dużo. Jednak ze względu na to, że wiele czynników jest zaangażowanych w wydajność, zalecamy, aby zawsze mierzyć rzeczywiste wyniki.

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 współużytkowanego w pętli równoległej, jak to możliwe, lub przynajmniej ograniczyć go. Najlepszym sposobem, aby to zrobić, jest użycie przeciążeń Parallel.For i Parallel.ForEach , które używają zmiennej System.Threading.ThreadLocal<T> do przechowywania stanu wątku lokalnego podczas wykonywania pętli. Aby uzyskać więcej informacji, zobacz How to: Write a Parallel.For Loop with Thread-Local Variables (Instrukcje: zapisywanie pętli Parallel.ForEach za pomocą zmiennych lokalnych partycji).

Unikaj nadmiernej równoległej obsługi

Korzystając z pętli równoległych, 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 zrównać pętli.

Najbardziej typowym scenariuszem, w którym może wystąpić nadmierna równoległizacja, jest pętle zagnieżdżone. W większości przypadków najlepiej jest zrównać tylko pętlę zewnętrzną, chyba że obowiązują co najmniej jeden z następujących warunków:

  • Pętla wewnętrzna jest znana jako bardzo długa.

  • 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 przetwarzanie.

We wszystkich przypadkach najlepszym sposobem określenia optymalnego kształtu zapytania jest przetestowanie i pomiar.

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

Zapisywanie w metodach wystąpienia niezwiązanych z wątkami z pętli równoległej 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.WriteByte jednocześnie, która nie jest obsługiwana przez klasę.

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

Ograniczanie wywołań do 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 używaj jej w pętlach równoległych, chyba że jest to konieczne.

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. Oznacza to na przykład, że nie można zaktualizować kontrolki listy z pętli równoległej, chyba że harmonogram wątków zostanie skonfigurowany do planowania pracy tylko w wątku interfejsu użytkownika. Aby uzyskać więcej informacji, zobacz Określanie kontekstu synchronizacji.

Zachowaj ostrożność podczas oczekiwania w delegatach, które są wywoływane przez parallel.Invoke

W pewnych okolicznościach biblioteka równoległa zadań będzie w tekście zadania, co oznacza, że jest ono uruchamiane w zadaniu w aktualnie wykonywanym wątku. (Aby uzyskać więcej informacji, zobacz Harmonogramy zadań). Ta optymalizacja wydajności może prowadzić do zakleszczenia w niektórych przypadkach. Na przykład dwa zadania mogą uruchamiać ten sam kod delegata, który sygnalizuje wystąpienie zdarzenia, a następnie czeka na sygnał drugiego zadania. Jeśli drugie zadanie zostanie wstawione w tym samym wątku co pierwszy, a pierwsze przejdzie w stan Oczekiwania, drugie zadanie nigdy nie będzie mogło zasygnalizować jego zdarzenia. Aby uniknąć takiego wystąpienia, możesz określić limit czasu operacji Wait lub użyć jawnych konstruktorów wątków, aby upewnić się, że jedno zadanie nie może zablokować drugiego.

Nie zakładaj, że iteracji forEach, for i ForAll always execute in Parallel

Należy pamiętać, że poszczególne iteracji w ForForEach pętli lub 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ć:

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

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.

Unikaj wykonywania pętli równoległych w wątku interfejsu użytkownika

Ważne jest, aby interfejs użytkownika aplikacji był dynamiczny. Jeśli operacja zawiera wystarczającą ilość pracy, aby uzasadnić równoległie, prawdopodobnie nie powinna być uruchamiana w wątku interfejsu użytkownika. Zamiast tego należy odciążyć tę operację do uruchomienia w wątku w tle. Jeśli na przykład chcesz użyć pętli równoległej do obliczenia niektórych danych, które powinny zostać renderowane w kontrolce interfejsu użytkownika, rozważ wykonanie pętli w wystąpieniu zadania, a nie bezpośrednio w procedurze obsługi zdarzeń interfejsu użytkownika. Tylko wtedy, gdy podstawowe obliczenia zostały ukończone, należy przeprowadzić marshaling aktualizacji interfejsu użytkownika z powrotem do wątku interfejsu użytkownika.

Jeśli uruchamiasz pętle równoległe w wątku interfejsu użytkownika, należy zachować ostrożność, aby uniknąć aktualizowania kontrolek interfejsu użytkownika z poziomu pętli. Próba zaktualizowania kontrolek interfejsu użytkownika z poziomu pętli równoległej wykonywanej w wątku interfejsu użytkownika może prowadzić do uszkodzenia stanu, wyjątków, opóźnionych aktualizacji, a nawet zakleszczeń, w zależności od sposobu wywoływanej aktualizacji interfejsu użytkownika. W poniższym przykładzie pętla równoległa blokuje wątek interfejsu użytkownika, na którym jest wykonywany do momentu ukończenia wszystkich iteracji. Jeśli jednak iteracja pętli jest uruchomiona w wątku w tle (jak For to możliwe), wywołanie wywołania Invoke powoduje przesłanie komunikatu do wątku interfejsu użytkownika i blokuje oczekiwanie na przetworzenie tego komunikatu. Ponieważ wątek interfejsu Forużytkownika jest zablokowany, nie można przetworzyć komunikatu i zakleszczenia wątku interfejsu użytkownika.

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

W poniższym przykładzie pokazano, jak uniknąć zakleszczenia, uruchamiając pętlę wewnątrz wystąpienia zadania. Wątek interfejsu użytkownika nie jest blokowany przez pętlę i można przetworzyć komunikat.

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

Zobacz też