Nękane przez błędy wielowątkowe

26

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:

  1. 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 .

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

  3. Szkolenie zespołu programistów na temat problemów z kodem wielowątkowym.

  4. Co byś zrobił?

koncurrency
źródło
23
Niektórzy ludzie, gdy napotkają problem, myślą, że użyję wielowątkowości. Teraz mają dwie sondy
Tom Squires
20
to, co działa dla mnie dobrze, to pozbycie się zmienności, gdy tylko jest to możliwe. Kiedy widzę, że zmienny obiekt zmienia stan w celu przekazania nowej wartości, staram się refaktoryzować to, aby przekazać nowy niezmienny obiekt zawierający zmienioną wartość. Jeśli inicjalizacja obiektu zostanie przeprowadzona bezpiecznie, gwarantuje to brak wyścigów danych - całkiem ulga
gnat
3
Witamy w wielowątkowym piekle. Od ponad 20 lat piszę programy wielowątkowe / równoległe w Ada, Occam, C ++. To nigdy nie jest łatwe, wszystko wymaga bardzo dokładnego przemyślenia, a każdy, kto mówi „łatwo jest po prostu zrób X”, jest głupcem, który tak naprawdę nie rozumie, co się dzieje. Powodzenia.
szybko_nie
2
Jeśli chcesz, aby współbieżność była sprawna, skorzystaj z Erlanga! W rzeczywistości to, czego chcesz, to jakaś forma wspólnego modelu aktora nic, w którym dziwne narożne przypadki zostaną wyeliminowane.
Zachary K,
3
@DeadMG Twierdziłbym, że współbieżność współdzielonego stanu jest z natury podatna na dziwne przypadki narożne i należy jej unikać. Ale Hej, napisałem książkę o Erlangu
Zachary K.

Odpowiedzi:

27

Twój kod ma inne istotne problemy poza tym. Ręcznie usuwasz wskaźnik? Wywoływanie cleanupfunkcji? 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 po DoSomethingImportantrzuceniu 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.

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 wywołań.

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.

DeadMG
źródło
To dobra odpowiedź, ale nie w tym kierunku, którego szukałem, ponieważ zbyt dużo czasu zajmuje ocenie fragmentu przykładowego kodu, który został napisany tylko dla uproszczenia (i nie odzwierciedla naszego prawdziwego kodu w naszym produkcie). Ale ciekawi mnie jeden komentarz, który napisałeś - „niewiarygodne narzędzia”. Co to jest niewiarygodne narzędzie? Jakie narzędzia polecasz?
koncurrency
5
@koncurrency: Niewiarygodne narzędzie to takie jak ręczne zarządzanie pamięcią lub pisanie własnej synchronizacji, w której teoretycznie rozwiązuje problem X, ale w rzeczywistości jest tak źle, że można zagwarantować gigantyczne błędy i jedyny sposób, w jaki mógłby rozwiązać problem pod ręką w rozsądnej skali jest ogromna i nieproporcjonalna inwestycja czasu programisty - właśnie to masz.
DeadMG
9

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.

Seth Noble
źródło
+1 za rozsądną, pośrednią sugestię dotyczącą przejścia przez istniejące wyzwanie.
Tak, to podejście badam. Podnosisz dobre punkty na temat wydajności.
koncurrency
Zmiana rzeczy do przejścia przez pojedynczy wątek wysyłkowy nie brzmi dla mnie jak szybka łatka, ale raczej dla mnie ogromny refaktor.
Sebastian Redl,
8

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.

Kevin Cline
źródło
Po prostu przeczytaj to teraz, kiedy moja odpowiedź została oceniona. Chciałem tylko powiedzieć, że ja uwielbiam zdanie wprowadzające :)
back2dos,
7

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

  1. Odbierz wiadomość
  2. Wykonaj obliczenia
  3. Wyślij wiadomość (y) / utwórz / zabij innych aktorów.

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ą.

Giorgio
źródło
1
Biblioteka Akka dla Scali jest fajną implementacją tego, która dużo myśli o tym, jak zabijać aktorów-rodziców, gdy dzieci umierają, i odwrotnie. Wiem, że to nie jest C ++, ale warto zajrzeć: akka.io
GlenPeterson
1
@GlenPeterson: Dzięki, wiem o akka (które uważam obecnie za najciekawsze rozwiązanie i działa zarówno z Javą, jak i Scalą), ale pytanie dotyczy konkretnie C ++. W przeciwnym razie można by pomyśleć o Erlangu. Wydaje mi się, że w Erlang wszystkie problemy związane z programowaniem wielowątkowym zniknęły na dobre. Ale może frameworki takie jak akka są bardzo podobne.
Giorgio
„Wydaje mi się, że w Erlang wszystkie problemy związane z programowaniem wielowątkowym zniknęły na dobre”. Myślę, że może to trochę zawyżone. Lub jeśli to prawda, wydajność może nie być wystarczająca. Wiem, że Akka nie współpracuje z C ++, mówiąc tylko, że wygląda to jak najnowocześniejsze narzędzie do zarządzania wieloma wątkami. Nie jest to jednak bezpieczne dla wątków. Nadal możesz przechodzić w stan zmienności między aktorami i strzelać sobie w stopę.
GlenPeterson
Nie jestem ekspertem Erlanga, ale AFAIK każdy aktor jest wykonywany w izolacji i niezmienne komunikaty są wymieniane. Więc tak naprawdę nie musisz w ogóle zajmować się wątkami i wspólnym stanem mutable. Wydajność jest prawdopodobnie niższa niż w C ++, ale zawsze dzieje się tak, gdy podnosisz poziom abstrakcji (zwiększasz czas wykonywania, ale skracasz czas programowania).
Giorgio
Czy downvoter może zostawić komentarz i zasugerować, jak mogę poprawić tę odpowiedź?
Giorgio,
6

Powszechny 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:

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. RolaFoo 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ć.

back2dos
źródło
2

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

  • Przekaż niezmienne przedmioty lub zrób ich kopie przed przejściem.
  • 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.)

Tim Williscroft
źródło
2

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

Erhannis
źródło
1

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.

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

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)

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ć.

Szkolenie zespołu programistów na temat problemów z kodem wielowątkowym.

Książka pomogłaby, ale myślę, że ćwiczenia / prototypowanie i doświadczony mentor byłyby lepsze.

Co byś zrobił?

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.

Klaim
źródło
Dzięki za sugestię książki. Prawdopodobnie to podniosę.
koncurrency
Gwintowanie jest naprawdę trudne. Nie każdy programista sprosta temu wyzwaniu. W świecie biznesu za każdym razem, gdy widziałem używane wątki, były one otoczone zamkami w taki sposób, że żadne dwa wątki nie mogły działać jednocześnie. Istnieją zasady, których możesz przestrzegać, aby to ułatwić, ale wciąż jest to trudne.
GlenPeterson
@GlenPeterson zgodził się, teraz, gdy mam więcej doświadczenia (od tej odpowiedzi), stwierdzam, że potrzebujemy lepszych abstrakcji, aby było możliwe do zarządzania i zniechęcało do udostępniania danych. Na szczęście projektanci języków wydają się ciężko nad tym pracować.
Klaim
Jestem pod wielkim wrażeniem Scali, szczególnie za to, że zapewniłem funkcjonalne korzyści z niezmienności, minimalne efekty uboczne w Javie, która jest bezpośrednim potomkiem C ++. Działa na wirtualnej maszynie Java, więc może nie mieć wymaganej wydajności. Książka Joshua Blocha „Skuteczna Java” dotyczy minimalizacji zmienności, tworzenia szczelnych interfejsów i bezpieczeństwa wątków. Mimo że jest oparty na Javie, założę się, że możesz zastosować 80-90% tego w C ++. Kwestionowanie zmienności i stanu wspólnego (lub zmienności stanu wspólnego) w recenzjach kodu może być dla Ciebie dobrym pierwszym krokiem.
GlenPeterson
1

Jesteś już w drodze, uznając problem i aktywnie szukając rozwiązania. Oto co bym zrobił:

  • Usiądź i zaprojektuj model gwintowania dla swojej aplikacji. To jest dokument, który odpowiada na pytania takie jak: Jakie masz rodzaje wątków? Co należy zrobić w jakim wątku? Jakiego rodzaju wzorców synchronizacji należy używać? Innymi słowy, powinien opisywać „zasady zaangażowania” podczas rozwiązywania problemów z wielowątkowością.
  • Użyj narzędzi do analizy wątków, aby sprawdzić bazę kodów pod kątem błędów. Valgrind ma moduł sprawdzania wątków o nazwie Helgrind, który jest dobry w wykrywaniu takich rzeczy, jak manipulowanie stanem wspólnym bez odpowiedniej synchronizacji. Z pewnością istnieją inne dobre narzędzia, poszukaj ich.
  • Rozważ migrację z C ++. C ++ jest koszmarem do pisania współbieżnych programów. Moim osobistym wyborem będzie Erlang , ale to kwestia gustu.
JesperE
źródło
8
Zdecydowanie -1 za ostatni bit. Wygląda na to, że kod OP używa najbardziej prymitywnych narzędzi, a nie rzeczywistych narzędzi C ++.
DeadMG,
2
Nie zgadzam się Współbieżność w C ++ to koszmar, nawet jeśli używasz odpowiednich mechanizmów i narzędzi C ++. I proszę zauważyć, że wybrałem sformułowanie „ rozważ ”. W pełni rozumiem, że może to nie być realistyczna alternatywa, ale pozostawanie w C ++ bez rozważania alternatyw jest po prostu głupie.
JesperE
4
@JesperE - przepraszam, ale nie. Współbieżność w C ++ to tylko koszmar, jeśli zrobisz to, przechodząc zbyt nisko. Użyj odpowiedniej abstrakcji wątków i nie będzie to gorsze niż jakikolwiek inny język lub środowisko wykonawcze. Przy odpowiedniej strukturze aplikacji jest to tak proste, jak wszystko, co kiedykolwiek widziałem.
Michael Kohne
2
W miejscu pracy uważam, że mamy odpowiednią strukturę aplikacji, używaj prawidłowych abstrakcji wątków itd. Mimo to spędziliśmy niezliczone godziny na debugowaniu błędów, które po prostu nie pojawiały się w językach odpowiednio zaprojektowanych pod kątem współbieżności. Mam jednak wrażenie, że będziemy musieli zgodzić się na to.
JesperE
1
@JesperE: Zgadzam się z tobą. Model Erlanga (dla którego istnieją implementacje dla Scala / Java, Ruby i, o ile mi wiadomo, również dla C ++) jest znacznie bardziej niezawodny niż kodowanie bezpośrednio za pomocą wątków.
Giorgio
1

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ę.

gnasher729
źródło
0

Co byś zrobił?

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ę.

Brendan
źródło