Najlepsze rozwiązania dotyczące zarządzanych wątków

Wielowątkowość wymaga starannego programowania. W przypadku większości zadań można zmniejszyć złożoność przez kolejkowanie żądań do wykonania przez wątki puli wątków. Ten temat dotyczy bardziej trudnych sytuacji, takich jak koordynowanie pracy wielu wątków lub obsługa wątków, które blokują.

Uwaga

Począwszy od programu .NET Framework 4, biblioteka równoległa zadań i PLINQ udostępniają interfejsy API, które zmniejszają złożoność i ryzyko programowania wielowątkowego. Aby uzyskać więcej informacji, zobacz Parallel Programming in .NET (Programowanie równoległe na platformie .NET).

Zakleszczenia i warunki wyścigu

Wielowątkowość rozwiązuje problemy z przepływnością i czasem reakcji, ale w ten sposób wprowadza nowe problemy: zakleszczenia i warunki wyścigu.

Zakleszczenia

Zakleszczenie występuje, gdy każdy z dwóch wątków próbuje zablokować zasób, który drugi został już zablokowany. Żaden z wątków nie może poczynić dalszych postępów.

Wiele metod zarządzanych klas wątków zapewnia limity czasu, aby ułatwić wykrywanie zakleszczeń. Na przykład poniższy kod próbuje uzyskać blokadę obiektu o nazwie lockObject. Jeśli blokada nie zostanie uzyskana w ciągu 300 milisekund, Monitor.TryEnter zwraca wartość false.

If Monitor.TryEnter(lockObject, 300) Then  
    Try  
        ' Place code protected by the Monitor here.  
    Finally  
        Monitor.Exit(lockObject)  
    End Try  
Else  
    ' Code to execute if the attempt times out.  
End If  
if (Monitor.TryEnter(lockObject, 300)) {  
    try {  
        // Place code protected by the Monitor here.  
    }  
    finally {  
        Monitor.Exit(lockObject);  
    }  
}  
else {  
    // Code to execute if the attempt times out.  
}  

Warunki wyścigu

Warunek wyścigu to usterka, która występuje, gdy wynik programu zależy od tego, który z co najmniej dwóch wątków osiągnie określony blok kodu jako pierwszy. Uruchomienie programu wiele razy generuje różne wyniki, a wynik jakiegokolwiek przebiegu nie można przewidzieć.

Prostym przykładem stanu wyścigu jest zwiększanie pola. Załóżmy, że klasa ma prywatne pole statyczne (udostępnione w Visual Basic), które jest zwiększane za każdym razem, gdy wystąpienie klasy jest tworzone, przy użyciu kodu takiego jak objCt++; (C#) lub objCt += 1 (Visual Basic). Ta operacja wymaga załadowania wartości z objCt do rejestru, przyrostowania wartości i przechowywania jej w pliku objCt.

W aplikacji wielowątkowej wątek, który załadował i zwiększył wartość, może zostać zastąpiony przez inny wątek, który wykonuje wszystkie trzy kroki; gdy pierwszy wątek wznawia wykonywanie i przechowuje jego wartość, zastępuje objCt ją bez uwzględniania faktu, że wartość zmieniła się w międzyczasie.

Ten konkretny warunek wyścigu można łatwo uniknąć przy użyciu metod Interlocked klasy, takich jak Interlocked.Increment. Aby zapoznać się z innymi technikami synchronizowania danych między wieloma wątkami, zobacz Synchronizowanie danych wielowątkowych.

Warunki wyścigu mogą również wystąpić, gdy synchronizujesz działania wielu wątków. Za każdym razem, gdy piszesz wiersz kodu, należy wziąć pod uwagę, co może się zdarzyć, jeśli wątek został wywłaszczone przed wykonaniem wiersza (lub przed dowolną instrukcją poszczególnych maszyn, które tworzą wiersz), a inny wątek go zastąpił.

Statyczne elementy członkowskie i konstruktory statyczne

Klasa nie jest inicjowana, dopóki konstruktor klasy (static konstruktor w języku C#, Shared Sub New w Visual Basic) nie zostanie uruchomiony. Aby zapobiec wykonywaniu kodu na typie, który nie jest zainicjowany, środowisko uruchomieniowe języka wspólnego blokuje wszystkie wywołania z innych wątków do static składowych klasy (Shared składowych w Visual Basic), dopóki konstruktor klasy nie zakończy działania.

Jeśli na przykład konstruktor klasy uruchamia nowy wątek, a procedura wątku wywołuje składową static klasy, nowe bloki wątku do momentu zakończenia konstruktora klasy.

Dotyczy to dowolnego typu, który może mieć static konstruktora.

Liczba procesorów

Niezależnie od tego, czy istnieje wiele procesorów, czy tylko jeden procesor dostępny w systemie, może mieć wpływ na architekturę wielowątkową. Aby uzyskać więcej informacji, zobacz Liczba procesorów.

Environment.ProcessorCount Użyj właściwości , aby określić liczbę procesorów dostępnych w czasie wykonywania.

Zalecenia ogólne

Podczas korzystania z wielu wątków należy wziąć pod uwagę następujące wskazówki:

  • Nie należy używać Thread.Abort do przerywania innych wątków. Wywoływanie Abort innego wątku jest związane z zgłaszaniem wyjątku w tym wątku, nie wiedząc, jaki punkt ten wątek osiągnął w jego przetwarzaniu.

  • Nie używaj funkcji Thread.Suspend i Thread.Resume do synchronizowania działań wielu wątków. MutexUżywaj , , ManualResetEvent, AutoResetEventi Monitor.

  • Nie steruj wykonywaniem wątków roboczych z głównego programu (na przykład przy użyciu zdarzeń). Zamiast tego zaprojektuj program tak, aby wątki robocze odpowiadały za oczekiwanie na dostępność pracy, wykonanie jej i powiadamianie innych części programu po zakończeniu. Jeśli wątki procesów roboczych nie blokują, rozważ użycie wątków puli wątków. Monitor.PulseAll jest przydatna w sytuacjach, w których bloki wątków roboczych.

  • Nie używaj typów jako obiektów blokady. Oznacza to, że należy unikać kodu, takiego jak lock(typeof(X)) w języku C# lub SyncLock(GetType(X)) Visual Basic, albo używać z Monitor.EnterType obiektami. W przypadku danego typu istnieje tylko jedno wystąpienie System.Type dla domeny aplikacji. Jeśli typ blokady jest publiczny, kod inny niż własny może przyjmować blokady, co prowadzi do zakleszczenia. Aby uzyskać dodatkowe problemy, zobacz Najlepsze rozwiązania dotyczące niezawodności.

  • Zachowaj ostrożność podczas blokowania wystąpień, na przykład lock(this) w języku C# lub SyncLock(Me) Visual Basic. Jeśli inny kod w aplikacji, poza typem, przyjmuje blokadę obiektu, mogą wystąpić zakleszczenia.

  • Upewnij się, że wątek, który wprowadził monitor, zawsze opuszcza ten monitor, nawet jeśli wystąpi wyjątek, gdy wątek znajduje się w monitorze. Instrukcja blokady języka C# i instrukcja SyncLock języka Visual Basic zapewniają to zachowanie automatycznie, stosując blokadę w końcu, aby upewnić się, że Monitor.Exit jest wywoływana. Jeśli nie możesz upewnić się, że zostanie wywołana funkcja Exit , rozważ zmianę projektu tak, aby korzystała z usługi Mutex. Mutex jest automatycznie zwalniany, gdy wątek, który jest obecnie właścicielem, kończy się.

  • Należy używać wielu wątków do zadań wymagających różnych zasobów i unikać przypisywania wielu wątków do jednego zasobu. Na przykład każde zadanie obejmujące operacje we/wy przynosi korzyści z posiadania własnego wątku, ponieważ ten wątek będzie blokowany podczas operacji we/wy, a tym samym zezwala na wykonywanie innych wątków. Dane wejściowe użytkownika to kolejny zasób, który korzysta z dedykowanego wątku. Na komputerze z jednym procesorem zadanie obejmujące intensywne obliczenia współistnieją z danymi wejściowymi użytkownika i zadaniami obejmującymi operacje we/wy, ale wiele zadań intensywnie korzystających z obliczeń zmaga się ze sobą.

  • Rozważ użycie metod klasy dla prostych Interlocked zmian stanu, zamiast używać lock instrukcji (SyncLock w Visual Basic). Instrukcja lock jest dobrym narzędziem ogólnego przeznaczenia, ale Interlocked klasa zapewnia lepszą wydajność aktualizacji, które muszą być niepodzielne. Wewnętrznie wykonuje pojedynczy prefiks blokady, jeśli nie ma rywalizacji. W przeglądach kodu poszukaj kodu, jak pokazano w poniższych przykładach. W pierwszym przykładzie zmienna stanu jest zwiększana:

    SyncLock lockObject  
        myField += 1  
    End SyncLock  
    
    lock(lockObject)
    {  
        myField++;  
    }  
    

    Wydajność można poprawić przy użyciu Increment metody zamiast instrukcji lock w następujący sposób:

    System.Threading.Interlocked.Increment(myField)  
    
    System.Threading.Interlocked.Increment(myField);  
    

    Uwaga

    Add Użyj metody dla niepodzielnych przyrostów większych niż 1.

    W drugim przykładzie zmienna typu odwołania jest aktualizowana tylko wtedy, gdy jest to odwołanie o wartości null (Nothing w Visual Basic).

    If x Is Nothing Then  
        SyncLock lockObject  
            If x Is Nothing Then  
                x = y  
            End If  
        End SyncLock  
    End If  
    
    if (x == null)  
    {  
        lock (lockObject)  
        {  
            x ??= y;
        }  
    }  
    

    Wydajność można poprawić przy użyciu CompareExchange metody w następujący sposób:

    System.Threading.Interlocked.CompareExchange(x, y, Nothing)  
    
    System.Threading.Interlocked.CompareExchange(ref x, y, null);  
    

    Uwaga

    Przeciążenie CompareExchange<T>(T, T, T) metody zapewnia bezpieczną dla typów referencyjnych alternatywę dla typów referencyjnych.

Rekomendacje dla bibliotek klas

Podczas projektowania bibliotek klas na potrzeby wielowątku należy wziąć pod uwagę następujące wskazówki:

  • Unikaj konieczności synchronizacji, jeśli to możliwe. Dotyczy to szczególnie intensywnie używanego kodu. Na przykład algorytm można dostosować tak, aby tolerował stan wyścigu, a nie go wyeliminować. Niepotrzebna synchronizacja zmniejsza wydajność i tworzy możliwość zakleszczenia i warunków wyścigu.

  • Domyślne zabezpieczanie wątków statycznych (Shared w języku Visual Basic).

  • Nie należy domyślnie zabezpieczać wątku danych wystąpienia. Dodanie blokad w celu utworzenia kodu bezpiecznego wątkowo zmniejsza wydajność, zwiększa rywalizację o blokadę i tworzy możliwość zakleszczenia. W typowych modelach aplikacji tylko jeden wątek jednocześnie wykonuje kod użytkownika, co minimalizuje potrzebę bezpieczeństwa wątków. Z tego powodu biblioteki klas platformy .NET nie są domyślnie bezpieczne wątkami.

  • Unikaj udostępniania metod statycznych, które zmieniają stan statyczny. W typowych scenariuszach serwera stan statyczny jest współużytkowany między żądaniami, co oznacza, że wiele wątków może wykonać ten kod w tym samym czasie. Otwiera to możliwość wątkowania usterek. Rozważ użycie wzorca projektowego, który hermetyzuje dane do wystąpień, które nie są współużytkowane przez żądania. Ponadto, jeśli dane statyczne są synchronizowane, wywołania między metodami statycznymi, które zmieniają stan, mogą spowodować zakleszczenia lub nadmiarową synchronizację, co negatywnie wpływa na wydajność.

Zobacz też