Dlaczego nie mogę sprawdzić, czy muteks jest zablokowany?

28

Wydaje się, że w C ++ 14 pominięto mechanizm sprawdzania, czy std::mutexblokada jest zablokowana, czy nie. Zobacz to SO pytanie:

/programming/21892934/how-to-assert-if-a-stdmutex-is-locked

Jest na to kilka sposobów, np. Przy użyciu;

std::mutex::try_lock()
std::unique_lock::owns_lock()

Ale żadne z nich nie jest szczególnie satysfakcjonującym rozwiązaniem.

try_lock()dozwolone jest zwracanie fałszywego ujemnego i ma niezdefiniowane zachowanie, jeśli bieżący wątek zablokował muteks. Ma również skutki uboczne. owns_lock()wymaga budowy unique_lockna górze oryginału std::mutex.

Oczywiście mógłbym stworzyć własne, ale wolałbym zrozumieć motywy obecnego interfejsu.

Możliwość sprawdzenia statusu muteksu (np. std::mutex::is_locked()) Nie wydaje mi się ezoteryczną prośbą, więc podejrzewam, że Komitet Standardowy celowo pominął tę funkcję, a nie przeoczenie.

Czemu?

Edycja: Ok, więc może ten przypadek użycia nie jest tak powszechny, jak się spodziewałem, więc zilustruję mój konkretny scenariusz. Mam algorytm uczenia maszynowego, który jest rozłożony na wiele wątków. Każdy wątek działa asynchronicznie i wraca do puli głównej po zakończeniu problemu z optymalizacją.

Następnie blokuje główny muteks. Wątek musi następnie wybrać nowego rodzica, od którego można mutować potomstwo, ale może wybrać tylko od rodziców, którzy obecnie nie mają potomstwa, które jest optymalizowane przez inne wątki. Dlatego muszę przeprowadzić wyszukiwanie, aby znaleźć rodziców, którzy nie są obecnie zablokowani przez inny wątek. Nie ma ryzyka zmiany statusu muteksu podczas wyszukiwania, ponieważ muteks głównego wątku jest zablokowany. Oczywiście istnieją inne rozwiązania (obecnie używam flagi logicznej), ale myślałem, że mutex oferuje logiczne rozwiązanie tego problemu, ponieważ istnieje on w celu synchronizacji między wątkami.

ilościowo
źródło
42
Naprawdę nie można właściwie sprawdzić, czy muteks jest zablokowany, ponieważ jedną nanosekundę po sprawdzeniu można go odblokować lub zablokować. Więc jeśli napisałeś „if (mutex_is_locked ()) ...”, to mutex_is_locked może zwrócić poprawny wynik, ale do czasu wykonania „if” jest źle.
gnasher729,
1
To ^. Jakie przydatne informacje masz nadzieję uzyskać is_locked?
Bezużyteczne
3
To wydaje się być problemem XY. Dlaczego starasz się zapobiec ponownemu wykorzystaniu rodziców tylko podczas rodzenia dziecka? Czy masz wymóg, aby którykolwiek rodzic mógł mieć tylko jedno potomstwo? Twój zamek tego nie zapobiegnie. Nie masz wyraźnych pokoleń? Jeśli nie, czy wiesz, że osoby, które można szybciej zoptymalizować, mają wyższą sprawność, ponieważ można je wybierać częściej / wcześniej? Jeśli używasz pokoleń, dlaczego nie wybierzesz wszystkich rodziców z góry, a następnie pozwól wątkom pobierać rodziców z kolejki? Czy generowanie potomstwa jest tak drogie, że potrzebujesz wielu wątków?
amon
10
@ quant - Nie rozumiem, dlaczego muteksy obiektów nadrzędnych w Twojej przykładowej aplikacji w ogóle muszą być muteksami: jeśli masz główny muteks, który jest blokowany za każdym razem, gdy są one ustawione, możesz po prostu użyć zmiennej boolowskiej, aby wskazać ich status.
Periata Breatta
4
Nie zgadzam się z ostatnim zdaniem pytania. Prosta wartość logiczna jest tutaj znacznie czystsza niż muteks. Zrób z tego atomowy bool, jeśli nie chcesz blokować głównego muteksu za „zwracanie” rodzica.
Sebastian Redl,

Odpowiedzi:

53

Widzę co najmniej dwa poważne problemy z sugerowaną operacją.

Pierwszy został już wspomniany w komentarzu @ gnasher729 :

Naprawdę nie można właściwie sprawdzić, czy muteks jest zablokowany, ponieważ jedną nanosekundę po sprawdzeniu można go odblokować lub zablokować. Więc jeśli napisałeś, if (mutex_is_locked ()) …to możesz mutex_is_lockedzwrócić poprawny wynik, ale do czasu wykonania ifjest on błędny.

Jedynym sposobem, aby upewnić się, że właściwość mutexu „jest obecnie zablokowana”, nie zmienia się, cóż, zablokować ją samodzielnie.

Drugi problem, jaki widzę, polega na tym, że dopóki nie zablokujesz muteksu, twój wątek nie zsynchronizuje się z wątkiem, który wcześniej zablokował muteks. Dlatego nie jest nawet dobrze zdefiniowane mówienie o „przed” i „po”, a to, czy muteks jest zamknięty, czy nie, jest rodzajem pytania, czy kot Schrödigera żyje bez próby otwarcia pudełka.

Jeśli dobrze rozumiem, oba problemy byłyby dyskusyjne w twoim konkretnym przypadku dzięki zablokowaniu głównego muteksu. Ale nie wydaje mi się to szczególnie częstym przypadkiem, więc uważam, że komitet postąpił słusznie, nie dodając funkcji, która może być nieco przydatna w bardzo szczególnych scenariuszach i powodować szkody we wszystkich innych. (W duchu: „Spraw, aby interfejsy były łatwe w użyciu i trudne w użyciu”).

I jeśli mogę powiedzieć, myślę, że konfiguracja, którą obecnie posiadasz, nie jest najbardziej elegancka i może zostać zrefaktoryzowana, aby całkowicie uniknąć problemu. Na przykład zamiast głównego wątku sprawdzającego wszystkich potencjalnych rodziców pod kątem takiego, który nie jest obecnie zablokowany, dlaczego nie utrzymywać kolejki gotowych rodziców? Jeśli wątek chce zoptymalizować inny, wyrzuca następny z kolejki i gdy tylko ma nowych rodziców, dodaje je do kolejki. W ten sposób nie potrzebujesz nawet głównego wątku jako koordynatora.

5gon12eder
źródło
Dzięki, to dobra odpowiedź. Powodem, dla którego nie chcę utrzymywać kolejki gotowych rodziców, jest to, że muszę zachować porządek, w jakim rodzice zostali stworzeni (ponieważ to decyduje o ich długości życia). Można to łatwo zrobić za pomocą kolejki LIFO. Jeśli zacznę szarpać i wysuwać rzeczy, będę musiał zachować osobny mechanizm porządkowania, który by komplikował rzeczy, stąd obecne podejście.
Quant
14
@ quant: Jeśli masz dwa cele w kolejce do rodziców, możesz to zrobić za pomocą dwóch kolejek ....
@ quant: Usuwasz element (co najwyżej) raz, ale prawdopodobnie wykonujesz przetwarzanie na każdym z nich wiele razy, więc optymalizujesz rzadki przypadek kosztem zwykłego przypadku. Jest to rzadko pożądane.
Jerry Coffin
2
Ale to jest uzasadnione, aby zapytać, czy bieżący wątek został zablokowany mutex.
Ograniczone Zadośćuczynienie
@LimitedAtonement Niezupełnie. Aby to zrobić, mutex musi przechowywać dodatkowe informacje (identyfikator wątku), co spowalnia je. Rekurencyjne muteksy już to robią, powinieneś je zamiast tego.
StaceyGirl,
9

Wygląda na to, że używasz wtórnych muteksów nie do blokowania dostępu do problemu optymalizacji, ale do ustalenia, czy problem optymalizacji jest obecnie optymalizowany czy nie.

To jest całkowicie niepotrzebne. Miałbym listę problemów, które wymagają optymalizacji, listę problemów, które są obecnie optymalizowane, oraz listę problemów, które zostały zoptymalizowane. (Nie bierz „listy” dosłownie, rozumiem przez to „odpowiednią strukturę danych).

Operacje dodawania nowego problemu do listy niezoptymalizowanych problemów lub przenoszenia problemu z jednej listy do następnej byłyby wykonywane pod ochroną pojedynczego muteksu „master”.

gnasher729
źródło
1
Nie sądzisz, że typ obiektu std::mutexjest odpowiedni dla takiej struktury danych?
Quant
2
@ quant - nie. std::mutexopiera się na implementacji mutex zdefiniowanej przez system operacyjny, która może dobrze zajmować zasoby (np. uchwyty), które są ograniczone i wolno przydzielają i / lub działają. Użycie pojedynczego muteksu do zablokowania dostępu do wewnętrznej struktury danych może być znacznie bardziej wydajne i być może również bardziej skalowalne.
Periata Breatta
1
Weź również pod uwagę zmienne stanu. Mogą sprawić, że wiele takich struktur danych będzie naprawdę łatwych.
Cort Ammon - Przywróć Monikę
2

Jak powiedzieli inni, nie ma przypadku użycia, w którym is_lockedmuteks przynosi jakiekolwiek korzyści, dlatego funkcja nie istnieje.

Sprawa masz problem jest bardzo powszechne, to w zasadzie co zrobić wątków roboczych, które są jednym z, jeśli nie z najczęstszych realizację wątkach.

Masz półkę z 10 pudełkami. Z tymi skrzynkami pracuje 4 pracowników. Jak upewnić się, że 4 pracowników pracuje na różnych skrzyniach? Pierwszy pracownik zdejmuje pudełko z półki, zanim zacznie nad nim pracować. Drugi pracownik widzi 9 pudełek na półce.

Nie ma muteksów do blokowania pudełek, więc widzenie stanu fikcyjnego muteksu na pudełku nie jest konieczne, a nadużywanie muteksu jako wartości logicznej jest po prostu błędne. Mutex blokuje półkę.

Piotr
źródło
1

Oprócz dwóch powodów podanych w odpowiedzi 5gon12eder powyżej, chciałbym dodać, że nie jest to ani konieczne, ani pożądane.

Jeśli już trzymasz muteks, to lepiej wiedzieć, że go trzymasz! Nie musisz pytać. Podobnie jak w przypadku posiadania bloku pamięci lub innego zasobu, powinieneś dokładnie wiedzieć, czy jesteś jego właścicielem, a kiedy należy zwolnić / usunąć zasób.
Jeśli tak nie jest, twój program jest źle zaprojektowany i masz kłopoty.

Jeśli chcesz uzyskać dostęp do współdzielonego zasobu chronionego przez muteks, a mutex nie jest już w posiadaniu, musisz go zdobyć. Nie ma innej opcji, w przeciwnym razie logika programu jest nieprawidłowa.
Może się okazać, że blokowanie jest akceptowalne lub niedopuszczalne, w obu przypadkach lock()lub try_lock()da pożądane zachowanie. Wszystko, co musisz wiedzieć, pozytywnie i bez wątpienia, to czy udało ci się zdobyć muteks (zwracana wartość try_lockmówi). Nie ma znaczenia, czy ktoś ją trzyma, czy też masz fałszywą porażkę.

W każdym innym przypadku, wprost, to nie twoja sprawa. Nie musisz wiedzieć, nie powinieneś wiedzieć ani przyjmować założeń (dotyczących terminowości i problemów z synchronizacją wymienionych w drugim pytaniu).

Damon
źródło
1
Co zrobić, jeśli chcę wykonać operację rankingu zasobów, które są obecnie dostępne do zablokowania?
Quant
Ale czy to jest coś realistycznego? Uznałbym to za raczej niezwykłe. Powiedziałbym, że oba zasoby mają już jakiś swoisty ranking, wtedy najpierw musisz zrobić (zdobyć blokadę) ważniejszy. Przykład: przed aktualizacją należy zaktualizować symulację fizyki. Lub, ranking jest mniej więcej celowy, wtedy możesz równie dobrze try_lockpierwszy zasób, a jeśli ten się nie powiedzie, wypróbuj drugi. Przykład: Trzy trwałe, połączone w pulę połączenia z serwerem bazy danych i musisz użyć jednego, aby wysłać polecenie.
Damon,
4
@quant - „operacja rankingowania zasobów, które są obecnie dostępne do zablokowania” - ogólnie rzecz biorąc, robienie tego rodzaju rzeczy jest naprawdę łatwym i szybkim sposobem na napisanie kodu, który blokuje się w sposób, który trudno jest zrozumieć. Określenie deterministycznego pozyskiwania i uwalniania zamków jest w prawie wszystkich przypadkach najlepszą polityką. Poszukiwanie zamka na podstawie kryterium, które mogłoby się zmienić, wymaga kłopotów.
Periata Breatta
@PeriataBreatta Mój program jest celowo nieokreślony. Widzę teraz, że ten atrybut nie jest powszechny, więc rozumiem, że pominięcie takich funkcji is_locked()może ułatwić takie zachowanie.
Quant
@ quant ranking i blokowanie są całkowicie osobnymi problemami. Jeśli chcesz jakoś posortować lub zmienić kolejkę z blokadą, zablokuj ją, posortuj, a następnie odblokuj. Jeśli potrzebujesz is_locked, istnieje znacznie lepsze rozwiązanie twojego problemu niż to, o którym myślisz.
Peter
1

Być może chcesz użyć atomic_flag z domyślną kolejnością pamięci. Nie ma wyścigów danych i nigdy nie zgłasza wyjątków, takich jak mutex z wieloma wywołaniami odblokowania (i przerywa niekontrolowane przerwanie, mógłbym dodać ...). Alternatywnie istnieje atomowy (np. Atomowy [bool] lub atomowy [int] (z nawiasami trójkątnymi, a nie [])), który ma ładne funkcje, takie jak load i Compare_exchange_strong.

Andrzej
źródło
1

Chcę dodać do tego przypadek użycia: Pozwoliłoby to funkcji wewnętrznej, aby zapewnić jako warunek wstępny / stwierdzenie, że dzwoniący rzeczywiście trzyma blokadę.

W przypadku klas z kilkoma takimi funkcjami wewnętrznymi i prawdopodobnie wieloma funkcjami publicznymi, które je wywołują, może zapewnić, że ktoś dodający inną funkcję publiczną wywołującą funkcję wewnętrzną rzeczywiście uzyskał blokadę.

class SynchronizedClass
{

public:

void publicFunc()
{
  std::lock_guard<std::mutex>(_mutex);

  internalFuncA();
}

// A lot of code

void newPublicFunc()
{
  internalFuncA(); // whops, forgot to acquire the lock
}


private:

void internalFuncA()
{
  assert(_mutex.is_locked_by_this_thread());

  doStuffWithLockedResource();
}

};
B3ret
źródło