W moim nowym zespole, którym zarządzam, większość naszego kodu to platforma, gniazdo TCP i kod sieci http. Wszystkie C ++. Większość pochodzi od innych programistów, którzy opuścili zespół. Obecni programiści w zespole są bardzo inteligentni, ale przede wszystkim młodsi pod względem doświadczenia.
Nasz największy problem: wielowątkowe błędy współbieżności. Większość naszych bibliotek klas jest zapisywanych jako asynchroniczne przy użyciu niektórych klas pul wątków. Metody z bibliotek klas często umieszczają w kolejce wątków kolejkę długiego działania w jednym wątku, a następnie metody wywołania zwrotnego tej klasy są wywoływane w innym wątku. W rezultacie mamy wiele błędów w przypadku krawędzi, które dotyczą nieprawidłowych założeń wątków. Powoduje to subtelne błędy, które wykraczają poza same krytyczne sekcje i blokady, aby uchronić się przed problemami z współbieżnością.
Tym, co sprawia, że problemy te są jeszcze trudniejsze, jest to, że próby naprawy są często nieprawidłowe. Niektóre błędy, które zaobserwowałem podczas próby zespołu (lub w obrębie samego starszego kodu), obejmują coś takiego:
Często występujący błąd nr 1 - Naprawianie problemu współbieżności poprzez blokadę współdzielonych danych, ale zapominając o tym, co się stanie, gdy metody nie zostaną wywołane w oczekiwanej kolejności. Oto bardzo prosty przykład:
void Foo::OnHttpRequestComplete(statuscode status)
{
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Mamy teraz błąd, w którym można było wywołać Shutdown podczas działania OnHttpNetworkRequestComplete. Tester znajduje błąd, przechwytuje zrzut awaryjny i przypisuje błąd do programisty. On z kolei naprawia błąd w ten sposób.
void Foo::OnHttpRequestComplete(statuscode status)
{
AutoLock lock(m_cs);
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
AutoLock lock(m_cs);
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Powyższa poprawka wygląda dobrze, dopóki nie zauważysz, że jest jeszcze bardziej subtelna obudowa. Co się stanie, jeśli Shutdown zostanie wywołany przed wywołaniem OnHttpRequestComplete? Przykłady ze świata rzeczywistego, które ma mój zespół, są jeszcze bardziej złożone, a przypadki skrajne są jeszcze trudniejsze do wykrycia podczas procesu przeglądu kodu.
Typowy błąd nr 2 - naprawianie problemów z zakleszczeniem poprzez ślepe wyjście z zamka, poczekanie na zakończenie drugiego wątku, a następnie ponowne wejście do zamka - ale bez obsługi przypadku, że obiekt został właśnie zaktualizowany przez inny wątek!
Typowy błąd nr 3 - Mimo że obiekty są liczone jako referencje, sekwencja zamykania „uwalnia” swój wskaźnik. Ale zapomina poczekać, aż wątek nadal działa, aby zwolnić jego instancję. W związku z tym komponenty są zamykane w sposób czysty, a następnie wywoływane są fałszywe lub spóźnione wywołania zwrotne na obiekcie w stanie, w którym nie oczekuje się więcej połączeń.
Istnieją inne przypadki krawędzi, ale sedno jest następujące:
Programowanie wielowątkowe jest po prostu trudne, nawet dla inteligentnych ludzi.
Gdy łapię te błędy, spędzam czas na omawianiu błędów z każdym programistą, aby opracować bardziej odpowiednią poprawkę. Podejrzewam jednak, że często mylą się, jak rozwiązać każdy problem z powodu ogromnej ilości starszego kodu, który wymaga poprawnego poprawienia.
Niedługo wysyłamy i jestem pewien, że łatki, które zastosujemy, będą obowiązywać w nadchodzącym wydaniu. Następnie będziemy mieli trochę czasu na ulepszenie bazy kodu i refaktoryzację w razie potrzeby. Nie będziemy mieli czasu, aby wszystko przepisać od nowa. A większość kodu nie jest taka zła. Ale szukam takiego refaktoryzacji kodu, aby całkowicie uniknąć problemów z wątkami.
Rozważam jedno podejście. Dla każdej ważnej funkcji platformy, należy mieć dedykowany pojedynczy wątek, w którym wszystkie zdarzenia i wywołania zwrotne w sieci zostają uporządkowane. Podobne do wątków w mieszkaniu COM w systemie Windows za pomocą pętli komunikatów. Długie operacje blokowania mogą być nadal wysyłane do wątku puli roboczej, ale w wątku komponentu wywoływane jest wywołanie zwrotne zakończenia. Komponenty mogłyby nawet dzielić ten sam wątek. Następnie wszystkie biblioteki klas działające w wątku można zapisać przy założeniu, że istnieje jeden świat wątków.
Zanim przejdę tą ścieżką, jestem również bardzo zainteresowany, czy istnieją inne standardowe techniki lub wzorce projektowe do radzenia sobie z problemami wielowątkowymi. I muszę podkreślić - coś poza książką, która opisuje podstawy muteksów i semaforów. Co myślisz?
Interesują mnie również wszelkie inne podejścia do procesu refaktoryzacji. W tym którekolwiek z poniższych:
Literatura lub artykuły na temat wzorów wokół nici. Coś poza wstępem do muteksów i semaforów. Nie potrzebujemy też masywnej równoległości, tylko sposoby zaprojektowania modelu obiektowego, aby poprawnie obsługiwać zdarzenia asynchroniczne z innych wątków .
Sposoby tworzenia schematów gwintowania różnych komponentów, aby łatwo było studiować i opracowywać rozwiązania. (To jest odpowiednik UML do omawiania wątków między obiektami i klasami)
Szkolenie zespołu programistów na temat problemów z kodem wielowątkowym.
Co byś zrobił?