1. Jak bezpiecznie zdefiniować?
Semantycznie. W tym przypadku nie jest to termin trudny do zdefiniowania. Oznacza to po prostu „Możesz to zrobić bez ryzyka”.
2. Jeśli program może być bezpiecznie wykonywany jednocześnie, czy zawsze oznacza to, że jest ponownie wysyłany?
Nie.
Na przykład, zastosujmy funkcję C ++, która przyjmuje zarówno blokadę, jak i wywołanie zwrotne jako parametr:
#include <mutex>
typedef void (*callback)();
std::mutex m;
void foo(callback f)
{
m.lock();
// use the resource protected by the mutex
if (f) {
f();
}
// use the resource protected by the mutex
m.unlock();
}
Inna funkcja może wymagać zablokowania tego samego muteksu:
void bar()
{
foo(nullptr);
}
Na pierwszy rzut oka wszystko wydaje się w porządku… Ale poczekaj:
int main()
{
foo(bar);
return 0;
}
Jeśli blokada muteksu nie jest rekurencyjna, oto, co się stanie, w głównym wątku:
main
zadzwoni foo
.
foo
przejmie zamek.
foo
zadzwoni bar
, co zadzwoni foo
.
- 2. miejsce
foo
spróbuje zdobyć zamek, zawiedzie i zaczeka na zwolnienie.
- Impas.
- Ups…
Ok, oszukiwałem, używając funkcji oddzwaniania. Łatwo jednak wyobrazić sobie bardziej złożone fragmenty kodu o podobnym działaniu.
3. Jaki dokładnie jest wspólny wątek między sześcioma wymienionymi punktami, o którym powinienem pamiętać, sprawdzając kod pod kątem możliwości ponownego wysłania?
Możesz poczuć problem, jeśli twoja funkcja ma / daje dostęp do modyfikowalnego trwałego zasobu lub ma / daje dostęp do funkcji, która wącha .
( Ok, 99% naszego kodu powinno pachnieć, a następnie… Zobacz ostatnią sekcję, aby sobie z tym poradzić… )
Tak więc, studiując kod, jeden z tych punktów powinien cię ostrzec:
- Funkcja ma stan (tj. Dostęp do zmiennej globalnej, a nawet zmiennej członka klasy)
- Ta funkcja może być wywoływana przez wiele wątków lub może pojawić się dwukrotnie na stosie podczas wykonywania procesu (tj. Funkcja może wywoływać się, bezpośrednio lub pośrednio). Funkcja odbiera oddzwanianie, ponieważ parametry dużo pachną .
Zauważ, że brak ponownego dostępu jest wirusowy: Funkcja, która mogłaby wywołać możliwą funkcję niezwracającą ponownego dostępu, nie może być uznana za ponowne wprowadzenie.
Zauważ też, że metody C ++ pachną, ponieważ mają do nich dostępthis
, dlatego powinieneś przestudiować kod, aby upewnić się, że nie występują w nich żadne dziwne interakcje.
4.1 Czy wszystkie funkcje rekurencyjne są ponownie wysyłane?
Nie.
W przypadkach wielowątkowych funkcja rekurencyjna uzyskująca dostęp do współdzielonego zasobu może być wywoływana przez wiele wątków w tym samym momencie, powodując złe / uszkodzone dane.
W przypadkach z pojedynczym wątkiem funkcja rekurencyjna może korzystać z funkcji bez ponownego udostępniania (jak niesławny strtok
) lub wykorzystywać dane globalne bez obsługi faktu, że dane są już w użyciu. Zatem twoja funkcja jest rekurencyjna, ponieważ wywołuje się bezpośrednio lub pośrednio, ale wciąż może być niebezpieczna rekurencyjnie .
4.2 Czy wszystkie funkcje wątkowo bezpieczne są ponownie wysyłane?
W powyższym przykładzie pokazałem, że pozornie bezpieczna funkcja nie była ponownie dostępna. OK, oszukiwałem z powodu parametru wywołania zwrotnego. Ale istnieje wiele sposobów zakleszczenia wątku poprzez uzyskanie dwukrotnie blokady nierekurencyjnej.
4.3 Czy wszystkie rekurencyjne i bezpieczne dla wątków funkcje są ponownie wysyłane?
Powiedziałbym „tak”, jeśli przez „rekurencyjny” masz na myśli „bezpieczny rekurencyjny”.
Jeśli możesz zagwarantować, że funkcja może być wywoływana jednocześnie przez wiele wątków i może wywoływać siebie bezpośrednio lub pośrednio, bez problemów, to jest ona ponownie wysyłana.
Problemem jest ocena tej gwarancji… ^ _ ^
5. Czy terminy takie jak ponowne wczytywanie i bezpieczeństwo nici są absolutne, tj. Czy mają ustalone konkretne definicje?
Wierzę, że tak, ale ocena funkcji jest bezpieczna dla wątków lub ponowne wysłanie może być trudne. Dlatego użyłem terminu zapach : możesz znaleźć, że funkcja nie jest ponownie dostępna, ale może być trudno mieć pewność, że złożony fragment kodu jest ponownie dostępny
6. Przykład
Załóżmy, że masz obiekt za pomocą jednej metody, która musi użyć zasobu:
struct MyStruct
{
P * p;
void foo()
{
if (this->p == nullptr)
{
this->p = new P();
}
// lots of code, some using this->p
if (this->p != nullptr)
{
delete this->p;
this->p = nullptr;
}
}
};
Pierwszy problem polega na tym, że jeśli w jakiś sposób funkcja ta zostanie wywołana rekurencyjnie (tj. Ta funkcja wywoła się bezpośrednio lub pośrednio), kod prawdopodobnie ulegnie awarii, ponieważ this->p
zostanie usunięty na końcu ostatniego wywołania i prawdopodobnie będzie używany przed końcem pierwszego połączenia.
Dlatego ten kod nie jest bezpieczny dla rekurencji .
Możemy użyć licznika referencyjnego, aby to naprawić:
struct MyStruct
{
size_t c;
P * p;
void foo()
{
if (c == 0)
{
this->p = new P();
}
++c;
// lots of code, some using this->p
--c;
if (c == 0)
{
delete this->p;
this->p = nullptr;
}
}
};
W ten sposób kod staje się bezpieczny dla rekurencyjnych… Ale wciąż nie jest ponownie wysyłany z powodu problemów z wielowątkowością: Musimy być pewni, że modyfikacje c
i p
zostaną wykonane atomowo przy użyciu rekurencyjnego muteksu (nie wszystkie muteksy są rekurencyjne):
#include <mutex>
struct MyStruct
{
std::recursive_mutex m;
size_t c;
P * p;
void foo()
{
m.lock();
if (c == 0)
{
this->p = new P();
}
++c;
m.unlock();
// lots of code, some using this->p
m.lock();
--c;
if (c == 0)
{
delete this->p;
this->p = nullptr;
}
m.unlock();
}
};
I oczywiście wszystko to zakłada lots of code
sam reentrant, w tym użycie p
.
Powyższy kod nie jest nawet zdalnie bezpieczny dla wyjątków , ale to już inna historia… ^ _ ^
7. Hej 99% naszego kodu nie jest ponownie wysyłane!
Jest to całkiem prawdziwe w przypadku kodu spaghetti. Ale jeśli podzielisz poprawnie swój kod, unikniesz problemów z ponownym uruchomieniem.
7.1 Upewnij się, że wszystkie funkcje mają stan NIE
Muszą używać parametrów, własnych zmiennych lokalnych, innych funkcji bez stanu i zwracać kopie danych, jeśli w ogóle zwracają.
7.2 Upewnij się, że Twój obiekt jest „bezpieczny dla rekurencji”
Metoda obiektowa ma dostęp do this
, więc dzieli stan ze wszystkimi metodami tej samej instancji obiektu.
Upewnij się więc, że obiekt może być używany w jednym punkcie stosu (tj. Wywołanie metody A), a następnie w innym punkcie (tj. Wywołanie metody B), bez niszczenia całego obiektu. Zaprojektuj swój obiekt, aby upewnić się, że po wyjściu z metody obiekt jest stabilny i poprawny (brak zwisających wskaźników, brak sprzecznych zmiennych składowych itp.).
7.3 Upewnij się, że wszystkie twoje obiekty są poprawnie zamknięte
Nikt inny nie powinien mieć dostępu do swoich danych wewnętrznych:
// bad
int & MyObject::getCounter()
{
return this->counter;
}
// good
int MyObject::getCounter()
{
return this->counter;
}
// good, too
void MyObject::getCounter(int & p_counter)
{
p_counter = this->counter;
}
Nawet zwrócenie stałego odwołania może być niebezpieczne, jeśli użytkownik pobierze adres danych, ponieważ inna część kodu mogłaby go zmodyfikować bez podania kodu stałego odwołania.
7.4 Upewnij się, że użytkownik wie, że Twój obiekt nie jest bezpieczny dla wątków
Dlatego użytkownik jest odpowiedzialny za stosowanie muteksów do używania obiektu współdzielonego między wątkami.
Obiekty z STL są zaprojektowane tak, aby nie były bezpieczne dla wątków (z powodu problemów z wydajnością), a zatem, jeśli użytkownik chce udostępnić std::string
dwa wątki, musi chronić swój dostęp za pomocą operacji podstawowych współbieżności;
7.5 Upewnij się, że kod bezpieczny dla wątków jest bezpieczny dla rekurencyjnych
Oznacza to używanie rekurencyjnych muteksów, jeśli uważasz, że ten sam zasób może być użyty dwukrotnie przez ten sam wątek.