Zarządzanie zadaniami asynchronicznymi i równoległymi
W przypadku deweloperów języka C# biblioteka równoległa zadań (TPL) ułatwia pisanie kodu równoległego. Jednak nie cały kod jest odpowiedni do przetwarzania równoległego. Jeśli na przykład pętla wykonuje tylko niewielką ilość pracy na każdej iteracji lub nie jest uruchamiana dla wielu iteracji, obciążenie związane z równoległością może spowodować, że kod działa wolniej. Ponadto równoległość, podobnie jak każdy kod wielowątkowy, zwiększa złożoność wykonywania programu.
Typowe pułapki dotyczące 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 proces równoległego wykonywania pętli wprowadza złożoność, która może prowadzić do problemów mniej powszechnych w kodzie sekwencyjnym.
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ą zasadą jest to, że pętle równoległe, które mają niewiele iteracji i szybko wykonywane delegaty użytkownika, prawdopodobnie nie przyspieszą znacząco. 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 często występuje odczytywanie lub zapisywanie wartości do zmiennych statycznych lub pól klas. Jednak za każdym razem, gdy wiele wątków uzyskuje dostęp do takich zmiennych jednocześnie, istnieje znaczący 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. Dlatego zalecamy unikać, lub przynajmniej ograniczać, dostęp do współdzielonego stanu w pętli równoległej, jak tylko to możliwe. Najlepszym sposobem jest użycie przeciążeń Parallel.For i Parallel.ForEach, które wykorzystują zmienną System.Threading.ThreadLocal<T> do przechowywania lokalnego stanu wątku podczas wykonywania pętli.
Unikaj nadmiernego równoległego przetwarzania
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 uzyskać przyspieszenia, uruchamiając wiele obliczeniowo intensywnych wątków na jednym procesorze. W związku z tym należy zachować ostrożność, aby nie nadmiernie zrównoleglić pętli.
Najbardziej typowym scenariuszem, w którym może wystąpić nadmierna równoległizacja, są pętle zagnieżdżone. W większości przypadków najlepiej jest równoleglić tylko pętlę zewnętrzną, chyba że dotyczy co najmniej jeden z następujących warunków:
- Wiadomo, że pętla wewnętrzna jest długa.
- Wykonujesz kosztowne obliczenia dla każdego zamówienia.
- System docelowy jest znany ze wystarczającej liczby procesorów do obsługi liczby wątków generowanych przez równoległe przetwarzanie.
We wszystkich przypadkach najlepszym sposobem określenia optymalnego kształtu zapytania jest przetestowanie i pomiar.
Obsługa wyjątków w zadaniach asynchronicznych i równoległych
W przypadku uruchamiania zadań przy użyciu biblioteki równoległej zadań (TPL) wyjątki mogą występować na kilka różnych sposobów. Najczęściej występuje, gdy zadanie zgłasza wyjątek. Zgłaszanie wyjątku może wystąpić, gdy zadanie jest uruchomione w wątku puli wątków lub gdy jest ono uruchomione w wątku głównym. W obu przypadkach wyjątek jest propagowany z powrotem do wątku wywołującego.
Jeśli używasz metody Task.Wait do oczekiwania na zakończenie zadania, wszystkie wyjątki zgłoszone przez to zadanie są przekazywane z powrotem do wątku, który je wywołał. Te wyjątki można obsługiwać przy użyciu bloku try/catch. Jeśli zadanie jest elementem nadrzędnym dołączonych zadań podrzędnych lub jeśli oczekujesz na wiele zadań, może zostać zgłoszonych wiele wyjątków. Jeśli zgłaszany jest co najmniej jeden wyjątek, są one opakowane w wystąpieniu AggregateException .
Wyjątek AggregateException ma właściwość InnerExceptions, którą można wyliczyć w celu zbadania wszystkich zgłoszonych oryginalnych wyjątków i obsługiwać (lub nie obsługiwać) każdego z osobna.
W poniższym przykładzie pokazano, jak obsługiwać wyjątki zgłaszane przez zadanie.
public static partial class Program
{
public static void Main()
{
HandleThree();
}
public static void HandleThree()
{
var task = Task.Run(
() => throw new CustomException("This exception is expected!"));
try
{
task.Wait();
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
{
// Handle the custom exception.
if (ex is CustomException)
{
Console.WriteLine(ex.Message);
}
// Rethrow any other exception.
else
{
throw ex;
}
}
}
}
}
// Define the CustomException class
public class CustomException : Exception
{
public CustomException(string message) : base(message) { }
}
// The example displays the following output:
// This exception is expected!
W tym przykładzie HandleThree metoda tworzy zadanie, które zgłasza błąd CustomException. Blok try/catch przechwytuje AggregateException i iteruje przez kolekcję InnerExceptions. Jeśli wyjątek jest typu CustomException, wyświetla komunikat do konsoli. Jeśli to inny typ wyjątku, zostanie on ponownie rzucony.
Można również obsługiwać oryginalne wyjątki przy użyciu AggregateException.Handle metody . Ta metoda przyjmuje delegata, który jest wywoływany dla każdego wyjątku w kolekcji InnerExceptions. Jeśli delegat zwróci wartość prawdziwą, wyjątek zostanie uznany za obsłużony i usunięty z kolekcji. Jeśli zwraca wartość false, wyjątek zostanie ponownie wyrzucony.
W poniższym przykładzie pokazano, jak używać Handle metody do obsługi wyjątków zgłaszanych przez zadanie.
public static partial class Program
{
public static void HandleFour()
{
var task = Task.Run(
() => throw new CustomException("This exception is expected!"));
try
{
task.Wait();
}
catch (AggregateException ae)
{
ae.Handle(ex =>
{
// Handle the custom exception.
if (ex is CustomException)
{
Console.WriteLine(ex.Message);
return true;
}
// Rethrow any other exception.
return false;
});
}
}
}
W tym przykładzie HandleFour metoda tworzy zadanie, które zgłasza błąd CustomException. Blok try/catch przechwytuje metodę AggregateException i wywołuje metodę Handle . Delegat sprawdza, czy wyjątek jest typu CustomException. Jeśli wyjątek jest typu CustomException, delegat wyświetla komunikat do konsoli i zwraca wartość true. Odpowiedź true wskazuje, że wyjątek został obsłużony. Jeśli wyjątek jest dowolnym innym typem wyjątku, delegat zwraca false, co powoduje ponowne przekazanie wyjątku.
Podsumowanie
W tej jednostce opisano sytuacje, w których kod nie jest odpowiedni do pracy równoległej, i omawia typowe pułapki w równoległym przetwarzaniu danych i zadań. Na przykład, założenie, że równoległe działanie jest zawsze szybsze, zapisywanie w lokalizacjach pamięci współdzielonej oraz nadmierna równoległość operacji. Zawartość wyjaśnia również, jak obsługiwać wyjątki w zadaniach asynchronicznych i równoległych, w tym jak używać metod Task.Wait i AggregateException.Handle.
Kwestie kluczowe
- Nie wszystkie kody są odpowiednie do przetwarzania równoległego. Testowanie i mierzenie wydajności jest niezbędne przed równoległym wykonaniem kodu.
- Typowe pułapki w równoległości danych i zadań obejmują założenie, że operacje równoległe są zawsze szybsze, zapisywanie w współdzielonych lokalizacjach pamięci i nadmierna równoległość przetwarzania.
- Wyjątki w zadaniach asynchronicznych i równoległych można obsłużyć przy użyciu metody
Task.Waiti metodyAggregateException.Handle. - Wyjątek
AggregateExceptionmaInnerExceptionswłaściwość, którą można wyliczyć, aby zbadać wszystkie oryginalne wyjątki, które zostały zgłoszone.