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ł?
źródło
Odpowiedzi:
Twój kod ma inne istotne problemy poza tym. Ręcznie usuwasz wskaźnik? Wywoływanie
cleanup
funkcji? Sowa Ponadto, jak dokładnie wskazano w komentarzu do pytania, nie używasz RAII do zamka, co jest kolejną dość epicką porażką i gwarantuje, że poDoSomethingImportant
rzuceniu wyjątku zdarzają się straszne rzeczy.Fakt, że występuje ten wielowątkowy błąd, jest tylko objawem podstawowego problemu - twój kod ma bardzo złą semantykę w każdej sytuacji wątkowania i używasz całkowicie niewiarygodnych narzędzi i ex-idiomów. Gdybym był tobą, byłbym zaskoczony, że działa z jednym wątkiem, nie mówiąc już o więcej.
Cały punkt odniesienia polega na tym, że wątek już zwolnił swoją instancję . Ponieważ jeśli nie, to nie można go zniszczyć, ponieważ wątek wciąż ma odwołanie.
Zastosowanie
std::shared_ptr
. Kiedy wszystkie wątki wydali (i nikt nie może więc być wywołanie funkcji, ponieważ nie mają one wskaźnik do niego), a następnie destruktor jest tzw. Gwarantuje to bezpieczeństwo.Po drugie, użyj prawdziwej biblioteki wątków, takiej jak bloki budujące wątki Intela lub biblioteka wzorców równoległych Microsoft. Pisanie własnego jest czasochłonne i niewiarygodne, a Twój kod jest pełen wątków, których nie potrzebuje. Robienie własnych blokad jest równie złe, jak zarządzanie pamięcią. Zaimplementowali już wiele bardzo przydatnych idiomów wątków, które działają poprawnie dla twojego zastosowania.
źródło
Inne plakaty dobrze komentowały, co należy zrobić, aby rozwiązać podstawowe problemy. Ten post dotyczy bardziej bezpośredniego problemu łatania starszego kodu na tyle, aby dać ci czas na ponowne wykonanie wszystkiego we właściwy sposób. Innymi słowy, nie jest to właściwy sposób na robienie rzeczy, to na razie tylko sposób, aby utykać.
Twój pomysł konsolidacji kluczowych wydarzeń to dobry początek. Posunąłbym się tak daleko, że użyłem pojedynczego wątku wysyłki do obsługi wszystkich kluczowych zdarzeń synchronizacji, wszędzie tam, gdzie istnieje zależność od zamówienia. Skonfiguruj bezpieczną dla wątków kolejkę komunikatów i wszędzie tam, gdzie obecnie wykonujesz operacje wrażliwe na współbieżność (alokacje, porządki, wywołania zwrotne itp.), Zamiast tego wyślij wiadomość do tego wątku i poproś ją o wykonanie lub wyzwolenie operacji. Chodzi o to, że ten jeden wątek kontroluje wszystkie uruchomienia, zatrzymania, przydziały i porządki jednostek roboczych.
Wątek wysyłki nie rozwiązuje opisanych przez ciebie problemów, po prostu konsoliduje je w jednym miejscu. Nadal musisz się martwić o zdarzenia / wiadomości pojawiające się w nieoczekiwanej kolejności. Zdarzenia o znacznym czasie działania będą nadal musiały być wysyłane do innych wątków, więc nadal występują problemy z współbieżnością współdzielonych danych. Jednym ze sposobów na złagodzenie tego jest uniknięcie przekazywania danych przez referencję. O ile to możliwe, dane w wiadomościach wysyłkowych powinny być kopiami, które będą własnością odbiorcy. (Jest to zgodne z zasadą uczynienia danych niezmiennymi, o których wspominali inni).
Zaletą tego podejścia do wysyłki jest to, że w wątku wysyłki istnieje rodzaj bezpiecznej przystani, w której przynajmniej wiesz, że pewne operacje następują sekwencyjnie. Wadą jest to, że tworzy wąskie gardło i dodatkowe obciążenie procesora. Sugeruję, aby na początku nie przejmować się żadną z tych rzeczy: najpierw skoncentruj się na uzyskaniu pewnej miary prawidłowego działania, przesuwając jak najwięcej do wątku wysyłkowego. Następnie wykonaj profilowanie, aby zobaczyć, co zajmuje najwięcej czasu procesora i zacznij przesuwać go z powrotem z wątku wysyłki, używając prawidłowych technik wielowątkowości.
Znowu to, co opisuję, nie jest właściwym sposobem na robienie rzeczy, ale jest to proces, który może poprowadzić cię we właściwą stronę w krokach, które są wystarczająco małe, aby dotrzymać terminów komercyjnych.
źródło
Na podstawie pokazanego kodu masz stos WTF. Naprawianie przyrostowe źle napisanej aplikacji wielowątkowej jest niezwykle trudne, jeśli nie niemożliwe. Poinformuj właścicieli, że aplikacja nigdy nie będzie niezawodna bez znacznych przeróbek. Podaj oszacowanie oparte na sprawdzeniu i ponownej obróbce każdego fragmentu kodu, który wchodzi w interakcję z obiektami współdzielonymi. Najpierw daj im szacunek do kontroli. Następnie możesz podać szacunkową wartość poprawki.
Kiedy przerobisz kod, powinieneś zaplanować napisanie kodu, aby był możliwy do poprawienia. Jeśli nie wiesz, jak to zrobić, znajdź kogoś, kto to zrobi, albo skończysz w tym samym miejscu.
źródło
Jeśli masz trochę czasu na refaktoryzację aplikacji, radzę spojrzeć na model aktora (patrz np. Theron , Casablanca , libcppa , CAF dla implementacji C ++).
Aktorzy to obiekty, które działają jednocześnie i komunikują się ze sobą tylko za pomocą asynchronicznej wymiany komunikatów. Tak więc wszystkie problemy związane z zarządzaniem wątkami, muteksami, zakleszczeniami itp. Są rozwiązywane przez bibliotekę implementacji aktorów i możesz skoncentrować się na implementacji zachowania swoich obiektów (aktorów), co sprowadza się do powtarzania pętli
Jednym z podejść może być najpierw przeczytanie tematu i ewentualnie przejrzenie jednej lub dwóch bibliotek, aby sprawdzić, czy model aktora można zintegrować z kodem.
Używam (uproszczonej wersji) tego modelu w moim projekcie od kilku miesięcy i jestem zdumiony jego solidnością.
źródło
Błędem tutaj nie jest „zapominanie”, ale „nie naprawianie go”. Jeśli coś dzieje się w nieoczekiwanej kolejności, masz problem. Powinieneś go rozwiązać, zamiast próbować go obejść (zatrzaskiwanie na czymś jest zwykle obejściem).
Powinieneś postarać się w pewnym stopniu dostosować model aktora / komunikat i mieć oddzielne obawy. Rola
Foo
jest oczywiście obsługa pewnego rodzaju komunikacji HTTP. Jeśli chcesz zaprojektować system tak, aby działał równolegle, to warstwa powyżej musi obsługiwać cykle życia obiektów i odpowiednio synchronizować dostęp.Próba uruchomienia wielu wątków na tych samych zmiennych danych jest trudna. Ale jest to również rzadko konieczne. Wszystkie typowe przypadki, które tego wymagają, zostały już streszczone w bardziej przystępne koncepcje i wdrożone wiele razy w odniesieniu do każdego ważnego imperatywnego języka. Musisz ich tylko użyć.
źródło
Twoje problemy są dość poważne, ale typowe dla złego użycia C ++. Przegląd kodu naprawi niektóre z tych problemów. 30 minut, jeden zestaw gałek ocznych generuje 90% wyników. (Można to znaleźć w Google)
# 1 Problem Musisz upewnić się, że istnieje ścisła hierarchia blokady, aby zapobiec zablokowaniu blokady.
Jeśli zastąpisz Autolock opakowaniem i makrem, możesz to zrobić.
Zachowaj statyczną globalną mapę blokad utworzonych z tyłu opakowania. Za pomocą makra wstawisz nazwę pliku i informacje o numerze wiersza do konstruktora opakowania Autolock.
Będziesz także potrzebować statycznego wykresu dominującego.
Teraz wewnątrz zamka musisz zaktualizować wykres dominujący, a jeśli otrzymasz zmianę zamówienia, popełnisz błąd i przerwiesz.
Po szeroko zakrojonych testach możesz pozbyć się większości ukrytych zakleszczeń.
Kod pozostawia się jako ćwiczenie dla ucznia.
Problem nr 2 zniknie (głównie)
Twoje archiwalne rozwiązanie zadziała. Używałem go już wcześniej w systemach misji i życia. Moje zdanie na ten temat jest takie
Nie udostępniaj danych za pośrednictwem zmiennych publicznych lub programów pobierających.
Zdarzenia zewnętrzne przychodzą poprzez wielowątkową wysyłkę do kolejki obsługiwanej przez jeden wątek. Teraz możesz w pewnym sensie uzasadnić obsługę zdarzeń.
Zmiany danych przechodzące przez wątki przechodzą w bezpieczną dla wątku sekwencję, są obsługiwane przez jeden wątek. Dokonuj subskrypcji. Teraz możesz posortować powód przepływów danych.
Jeśli dane muszą przejść przez miasto, opublikuj je w kolejce danych. Spowoduje to skopiowanie go i przekazanie subskrybentom asynchronicznie. Przerywa także wszystkie zależności danych w programie.
To jest w zasadzie model aktora na tanie. Linki Giorgio pomogą.
Wreszcie twój problem z zamykanymi obiektami.
Podczas liczenia referencji rozwiązałeś 50%. Pozostałe 50% to odliczanie oddzwonień. Przekaż posiadaczom oddzwonienia refernce. Połączenie zamykające musi wtedy czekać na zerowe zliczanie na koncie. Nie rozwiązuje skomplikowanych wykresów obiektowych; dostanie się do prawdziwego śmiecia. (Co jest motywacją w Javie, aby nie składać żadnych obietnic dotyczących tego, kiedy lub czy zostanie wywołane finalizowanie (); aby wyciągnąć cię z programowania w ten sposób.)
źródło
Dla przyszłych odkrywców: aby uzupełnić odpowiedź na temat modelu aktora, chciałbym dodać CSP ( komunikację procesów sekwencyjnych ), z ukłonem w stronę większej rodziny kalkulatorów procesów, w których się znajduje. CSP jest podobny do modelu aktora, ale różni się w podziale. Nadal masz kilka wątków, ale komunikują się one za pomocą określonych kanałów, a nie ze sobą, a oba procesy muszą być gotowe do odpowiedniego wysłania i odebrania, zanim którekolwiek z nich się zdarzy. Istnieje również sformalizowany język do sprawdzania poprawności kodu CSP. Nadal przechodzę do intensywnego korzystania z CSP, ale korzystam z niego w kilku projektach od kilku miesięcy i jest to bardzo uproszczone.
University of Kent ma implementację C ++ ( https://www.cs.kent.ac.uk/projects/ofa/c++csp/ , sklonowany na https://github.com/themasterchef/cppcsp2 ).
źródło
Obecnie czytam to i wyjaśniam wszystkie problemy, które możesz uzyskać, i jak ich uniknąć, w C ++ (przy użyciu nowej biblioteki wątków, ale myślę, że globalne wyjaśnienia są ważne w twoim przypadku): http: //www.amazon. com / C-Concurrency-Action-Practical-Multithreading / dp / 1933988770 / ref = sr_1_1? ie = UTF8 & qid = 1337934534 & sr = 8-1
Osobiście używam uproszczonego UML i po prostu zakładam, że wiadomości są wykonywane asynchronicznie. Jest to również prawdą między „modułami”, ale wewnątrz modułów nie chcę o tym wiedzieć.
Książka pomogłaby, ale myślę, że ćwiczenia / prototypowanie i doświadczony mentor byłyby lepsze.
Całkowicie unikałbym, aby ludzie nie rozumiejący problemów z współbieżnością pracowali nad projektem. Ale myślę, że nie możesz tego zrobić, więc w twoim konkretnym przypadku, poza staraniem się, aby zespół był lepiej wykształcony, nie mam pojęcia.
źródło
Jesteś już w drodze, uznając problem i aktywnie szukając rozwiązania. Oto co bym zrobił:
źródło
Patrząc na twój przykład: jak tylko Foo :: Shutdown zacznie działać, nie może być możliwe wywołanie OnHttpRequestComplete, aby uruchomić. To nie ma nic wspólnego z żadną implementacją, po prostu nie działa.
Można również argumentować, że Foo :: Shutdown nie powinien być wywoływalny, gdy uruchomione jest wywołanie OnHttpRequestComplete (zdecydowanie prawda) i prawdopodobnie nie, jeśli wywołanie OnHttpRequestComplete jest nadal nierozstrzygnięte.
Pierwszą rzeczą, którą należy zrobić, to nie blokowanie itd., Ale logika tego, co jest dozwolone, czy nie. Prostym modelem byłoby, że twoja klasa może mieć zero lub więcej niekompletnych żądań, zero lub więcej uzupełnień, które nie zostały jeszcze wywołane, zero lub więcej ukończonych uruchomień, a twój obiekt chce się zamknąć lub nie.
Foo :: Shutdown powinien kończyć uruchamianie ukończeń, uruchamiać niekompletne żądania do punktu, w którym można je zamknąć, jeśli to możliwe, nie zezwalać na uruchamianie kolejnych ukończeń, nie zezwalać na uruchamianie kolejnych żądań.
Co musisz zrobić: dodaj specyfikacje do swoich funkcji, mówiąc dokładnie, co będą robić. (Na przykład uruchomienie żądania HTTP może się nie powieść po wywołaniu haseł Shutdown). A potem napisz swoje funkcje, aby spełniały specyfikacje.
Zamki najlepiej stosować tylko na możliwie najkrótszy czas, aby kontrolować modyfikację wspólnych zmiennych. Więc możesz mieć zmienną „performShutDown”, która jest chroniona przez blokadę.
źródło
Szczerze; Uciekłbym szybko.
Problemy z współbieżnością są NASTĘPNE . Coś może działać idealnie przez miesiące, a następnie (z powodu specyficznego czasu kilku rzeczy) nagle wysadzają się w twarz klienta, nie ma sposobu, aby dowiedzieć się, co się stało, nie ma nadziei na zobaczenie miłego (powtarzalnego) zgłoszenia błędu i nie ma mowy aby się upewnić, że nie była to usterka sprzętowa, która nie ma nic wspólnego z oprogramowaniem.
Unikanie problemów z współbieżnością musi rozpocząć się na etapie projektowania, zaczynając dokładnie od tego, jak to zrobisz („globalna kolejność blokowania”, model aktora, ...). Nie jest to coś, co próbujesz naprawić w szalonej panice w nadziei, że wszystko nie ulegnie samozniszczeniu po nadchodzącym wydaniu.
Pamiętaj, że nie żartuję tutaj. Twoje własne słowa („ Większość pochodzi od innych programistów, którzy opuścili zespół. Obecni programiści w zespole są bardzo mądrzy, ale przede wszystkim młodsi pod względem doświadczenia. ”) Wskazują, że wszyscy ludzie już zrobili to, co ja sugeruję.
źródło