Is std::lock() ill-defined, unimplementable, or useless?

(Uwaga: wiele z tego jest zbędnych w przypadku komentarza na tematOgromne obciążenie procesora przy użyciu std :: lock (c ++ 11), ale myślę, że ten temat zasługuje na własne pytanie i odpowiedzi.)

Niedawno napotkałem przykładowy kod C ++ 11, który wyglądał mniej więcej tak:

std::unique_lock<std::mutex> lock1(from_acct.mutex, std::defer_lock);
std::unique_lock<std::mutex> lock2(to_acct.mutex, std::defer_lock);
std::lock(lock1, lock2); // avoid deadlock
transfer_money(from_acct, to_acct, amount);

Wow, pomyślałem,std::lock brzmi interesująco. Ciekawe, co mówi ten standard?

C ++ 11 sekcja 30.4.3 [thread.lock.algorithm], akapity (4) i (5):

blokada szablonu pustka (L1 i, L2 i, L3 i ...);

4 Wymaga: Każdy typ parametru szablonu musi spełniać wymagania dotyczące blokady, [Uwaga: Theunique_lock szablon klasy spełnia te wymagania po odpowiednim utworzeniu instancji. - nota końcowa]

5 Ruchomości: Wszystkie argumenty są zablokowane przez sekwencję wywołańlock(), try_lock()lubunlock() na każdy argument. Sekwencja wywołań nie spowoduje zakleszczenia, ale jest inaczej nieokreślona. [Uwaga: Należy zastosować algorytm unikania zakleszczenia, taki jak try-and-back-off, ale określony algorytm nie jest określony, aby uniknąć implementacji nadmiernie ograniczających. - Uwaga końcowa] Jeśli zadzwonisz dolock() lubtry_lock() rzuca wyjątekunlock() zostanie wezwany do jakiegokolwiek argumentu, który został zablokowany przez wezwanielock() lubtry_lock().

Rozważmy następujący przykład. Nazwij to „Przykład 1”:

Thread 1                    Thread 2
std::lock(lock1, lock2);    std::lock(lock2, lock1);

Czy ten impas?

Prosty odczyt standardu mówi „nie”. Świetny! Może kompilator może zamówić dla mnie moje zamki, co byłoby w porządku.

Teraz spróbuj Przykład 2:

Thread 1                                  Thread 2
std::lock(lock1, lock2, lock3, lock4);    std::lock(lock3, lock4);
                                          std::lock(lock1, lock2);

Czy ten impas?

Ponownie, zwykły odczyt standardu mówi „nie”. O o. Jedynym sposobem, aby to zrobić, jest zastosowanie pętli back-off-and-retry. Więcej na ten temat poniżej.

Wreszcie przykład 3:

Thread 1                          Thread 2
std::lock(lock1,lock2);           std::lock(lock3,lock4);
std::lock(lock3,lock4);           std::lock(lock1,lock2);

Czy ten impas?

Ponownie zwykły odczyt standardu mówi „nie”. (Jeśli „sekwencja połączeń dolock()„w jednym z tych wywołań nie jest„ skutkiem impasu ”, co to jest dokładnie?) Jednak jestem całkiem pewien, że jest to niemożliwe do wdrożenia, więc przypuszczam, że nie o to im chodziło.

To wydaje się być jedną z najgorszych rzeczy, jakie kiedykolwiek widziałem w standardzie C ++. Domyślam się, że to był ciekawy pomysł: niech kompilator przypisze kolejność blokowania. Ale gdy komisja go przeżuje, wynik jest albo niewykonalny, albo wymaga ponownej pętli. I tak, to zły pomysł.

Możesz argumentować, że „cofnij się i ponów” jest czasami przydatne. To prawda, ale tylko wtedy, gdy nie wiesz, które zamki próbujesz złapać z przodu. Na przykład, jeśli tożsamość drugiego zamka zależy od danych chronionych przez pierwszą (powiedzmy, ponieważ przechodzisz przez jakąś hierarchię), to być może będziesz musiał wykonać pewne spin-grab-release-grab. Ale w takim przypadku nie możesz użyć tego gadżetu, ponieważ nie znasz wszystkich blokad z przodu. Z drugiej strony, jeśli tyrobić wiesz, który blokuje cię z góry, wtedy (prawie) zawsze chcesz po prostu narzucić porządek, a nie zapętlić.

Należy również zauważyć, że przykład 1 może zostać zablokowany na żywo, jeśli implementacja po prostu chwyta zamki w kolejności, wycofuje się i ponawia próby.

Krótko mówiąc, ten gadżet wydaje mi się w najlepszym razie bezużyteczny. Po prostu zły pomysł dookoła.

OK, pytania. (1) Czy moje roszczenia lub interpretacje są błędne? (2) Jeśli nie, co oni do cholery myślą? (3) Czy wszyscy powinniśmy się zgodzić, że „najlepszą praktyką” jest unikaniestd::lock całkowicie?

[Aktualizacja]

Niektóre odpowiedzi mówią, że źle interpretuję standard, a następnie interpretuję go w ten sam sposób, w jaki to zrobiłem, a następnie mylę specyfikację z implementacją.

Tak więc, żeby było jasne:

W moim odczycie standardu przykład 1 i przykład 2 nie mogą się zakleszczyć. Przykład 3 może, ale tylko dlatego, że uniknięcie zakleszczenia w takim przypadku jest niewykonalne.

Cały mój problem polega na tym, że unikanie zakleszczenia w przykładzie 2 wymaga pętli „ponów próbę” i takie pętle są wyjątkowo słabą praktyką. (Tak, jakaś statyczna analiza tego trywialnego przykładu może sprawić, że będzie to możliwe do uniknięcia, ale nie w ogólnym przypadku.) Należy również zauważyć, że GCC implementuje tę rzecz jako pętlę zajęty.

[Aktualizacja 2]

Myślę, że wiele rozłączeń jest podstawową różnicą w filozofii.

Istnieją dwa podejścia do pisania oprogramowania, zwłaszcza oprogramowania wielowątkowego.

W jednym podejściu rzucasz razem kilka rzeczy i uruchamiasz je, aby zobaczyć, jak to działa. Nigdy nie jesteś przekonany, że twój kod ma problem, chyba że ktoś może zademonstrować ten problem w prawdziwym systemie, właśnie teraz.

W drugim podejściu piszesz kod, który można rygorystycznie przeanalizować, aby udowodnić, że nie ma wyścigów danych, że wszystkie jego pętle kończą się z prawdopodobieństwem 1 i tak dalej. Analizę tę wykonuje się ściśle w modelu maszyny gwarantowanym przez specyfikację języka, a nie w konkretnej implementacji.

Zwolennicy tego drugiego podejścia nie są pod wrażeniem jakichkolwiek demonstracji na temat poszczególnych procesorów, kompilatorów, wersji pomocniczych kompilatora, systemów operacyjnych, środowisk wykonawczych itp. Takie demonstracje są ledwo interesujące i całkowicie nieistotne. Jeżeli twójalgorytm ma wyścig danych, jest uszkodzony, bez względu na to, co się stanie, gdy go uruchomisz. Jeżeli twójalgorytm ma blokadę, jest uszkodzony, niezależnie od tego, co się stanie, gdy go uruchomisz. I tak dalej.

W moim świecie drugie podejście nazywa się „Inżynierią”. Nie jestem pewien, jak nazywa się pierwsze podejście.

O ile wiem,std::lock interfejs jest bezużyteczny dla inżynierii. Bardzo bym chciał udowodnić, że się mylę.

questionAnswers(4)

yourAnswerToTheQuestion