Threadsafe vs re-entrant

Odpowiedzi:

42

Funkcje re-entrant nie opierają się na zmiennych globalnych, które są uwidocznione w nagłówkach biblioteki C. Weź strtok () vs strtok_r () na przykład w C.

Niektóre funkcje wymagają miejsca na przechowywanie „pracy w toku”, a funkcje ponownego wejścia pozwalają określić ten wskaźnik we własnej pamięci wątku, a nie w globalnej. Ponieważ ta pamięć jest wyłączna dla funkcji wywołującej, może zostać przerwana i ponownie wprowadzona (ponowne wejście), a ponieważ w większości przypadków wzajemne wykluczenie poza to, co implementuje funkcja, nie jest wymagane, aby to zadziałało, często uważa się je za bezpieczny wątek . Nie jest to jednak gwarantowane z definicji.

errno jest jednak nieco innym przypadkiem w systemach POSIX (i wydaje się dziwacznym wyjaśnieniem, jak to wszystko działa) :)

Krótko mówiąc, ponowne wejście często oznacza bezpieczne dla wątków (tak jak w przypadku „użyj wersji tej funkcji, jeśli używasz wątków”), ale bezpieczeństwo wątków nie zawsze oznacza ponowne wejście (lub odwrotnie). Kiedy patrzysz na bezpieczeństwo wątków, musisz pomyśleć o współbieżności . Jeśli musisz zapewnić środki do blokowania i wzajemnego wykluczania, aby używać funkcji, funkcja nie jest z natury bezpieczna dla wątków.

Ale nie wszystkie funkcje muszą być sprawdzane. malloc()nie ma potrzeby ponownego wchodzenia, nie zależy od niczego poza zakresem punktu wejścia dla danego wątku (i sam jest bezpieczny dla wątków).

Funkcje, które zwracają wartości przydzielone statycznie, nie są bezpieczne dla wątków bez użycia mechanizmu mutex, futex lub innego atomowego mechanizmu blokującego. Jednak nie muszą ponownie wchodzić, jeśli nie mają być przerywane.

to znaczy:

static char *foo(unsigned int flags)
{
  static char ret[2] = { 0 };

  if (flags & FOO_BAR)
    ret[0] = 'c';
  else if (flags & BAR_FOO)
    ret[0] = 'd';
  else
    ret[0] = 'e';

  ret[1] = 'A';

  return ret;
}

Tak więc, jak widać, korzystanie z tego przez wiele wątków bez jakiegoś rodzaju blokowania byłoby katastrofą ... ale ponowne wejście nie ma na celu. Napotkasz to, gdy dynamicznie przydzielana pamięć jest tabu na niektórych wbudowanych platformach.

W programowaniu czysto funkcjonalnym ponowne wejście często nie oznacza bezpieczeństwa wątków, zależałoby to od zachowania zdefiniowanych lub anonimowych funkcji przekazanych do punktu wejścia funkcji, rekurencji itp.

Lepszym sposobem na zapewnienie bezpieczeństwa wątków jest bezpieczeństwo w przypadku dostępu równoległego , co lepiej ilustruje tę potrzebę.

Tim Post
źródło
2
Reentrant nie oznacza, że ​​jest bezpieczny dla wątków. Czyste funkcje oznaczają bezpieczeństwo wątków.
Julio Guerra
Świetna odpowiedź Tim. Dla wyjaśnienia, z twojego „często” rozumiem, że bezpieczne dla wątków nie oznacza ponownego wejścia, ale również ponowne wejście nie oznacza bezpieczeństwa wątków. Czy byłbyś w stanie znaleźć przykład funkcji ponownego wejścia, która nie jest bezpieczna dla wątków?
Riccardo,
@ Tim Post „Krótko mówiąc, ponowne wejście często oznacza bezpieczeństwo wątków (jak w przypadku„ użyj wersji tej funkcji, jeśli używasz wątków ”), ale bezpieczeństwo wątków nie zawsze oznacza ponowne wejście.” qt mówi odwrotnie: „W związku z tym funkcja bezpieczna dla wątków jest zawsze wprowadzana ponownie, ale funkcja ponownie wprowadzana nie zawsze jest bezpieczna dla wątków”.
4pie0
a wikipedia mówi jeszcze coś: „Ta definicja ponownego wejścia różni się od definicji bezpieczeństwa wątków w środowiskach wielowątkowych. Podprogram reentrant może zapewnić bezpieczeństwo wątków [1], ale samo ponowne wejście może nie wystarczyć do zapewnienia bezpieczeństwa wątków we wszystkich sytuacjach. Odwrotnie, kod bezpieczny dla wątków niekoniecznie musi być ponownie wprowadzany (...) "
4pie0
@ Riccardo: Funkcje zsynchronizowane za pomocą zmiennych ulotnych, ale nie pełne bariery pamięci do użycia z programami obsługi sygnału / przerwań, są zwykle ponownie wchodzące, ale są bezpieczne dla wątków.
doynax
77

TL; DR: Funkcja może być ponownie wprowadzana, bezpieczna wątkowo, obie lub żadne.

Warto przeczytać artykuły Wikipedii dotyczące bezpieczeństwa wątków i ponownego wejścia na serwer . Oto kilka cytatów:

Funkcja jest bezpieczna wątkowo, jeśli:

manipuluje tylko współdzielonymi strukturami danych w sposób gwarantujący bezpieczne wykonywanie przez wiele wątków w tym samym czasie.

Funkcja jest ponownie wprowadzana, jeśli:

może zostać przerwane w dowolnym momencie podczas wykonywania, a następnie bezpiecznie wywołane ponownie („wprowadzone ponownie”), zanim poprzednie wywołania zakończą się.

Jako przykłady możliwego ponownego wejścia Wikipedia podaje przykład funkcji przeznaczonej do wywołania przez przerwania systemowe: załóżmy, że jest już uruchomiona, gdy nastąpi kolejne przerwanie. Ale nie myśl, że jesteś bezpieczny tylko dlatego, że nie kodujesz z przerwaniami systemowymi: możesz mieć problemy z ponownym wejściem w programie jednowątkowym, jeśli używasz wywołań zwrotnych lub funkcji rekurencyjnych.

Kluczem do uniknięcia nieporozumień jest to, że ponowne wejście odnosi się do wykonywania tylko jednego wątku. To koncepcja z czasów, gdy nie istniały żadne wielozadaniowe systemy operacyjne.

Przykłady

(Nieznacznie zmodyfikowane na podstawie artykułów Wikipedii)

Przykład 1: nie jest bezpieczny dla wątków, nie jest ponownie wprowadzany

/* As this function uses a non-const global variable without
   any precaution, it is neither reentrant nor thread-safe. */

int t;

void swap(int *x, int *y)
{
    t = *x;
    *x = *y;
    *y = t;
}

Przykład 2: bezpieczne dla wątków, niewprowadzane ponownie

/* We use a thread local variable: the function is now
   thread-safe but still not reentrant (within the
   same thread). */

__thread int t;

void swap(int *x, int *y)
{
    t = *x;
    *x = *y;
    *y = t;
}

Przykład 3: brak bezpieczeństwa wątków, ponowne wejście

/* We save the global state in a local variable and we restore
   it at the end of the function.  The function is now reentrant
   but it is not thread safe. */

int t;

void swap(int *x, int *y)
{
    int s;
    s = t;
    t = *x;
    *x = *y;
    *y = t;
    t = s;
}

Przykład 4: bezpieczeństwo wątków, ponowne wejście

/* We use a local variable: the function is now
   thread-safe and reentrant, we have ascended to
   higher plane of existence.  */

void swap(int *x, int *y)
{
    int t;
    t = *x;
    *x = *y;
    *y = t;
}
MiniQuark
źródło
10
Wiem, że nie powinienem komentować tylko w celu podziękowania, ale jest to jedna z najlepszych ilustracji przedstawiających różnice między funkcjami ponownego wejścia i bezpiecznymi wątkami. W szczególności użyłeś bardzo zwięzłych, jasnych terminów i wybrałeś świetną przykładową funkcję do rozróżnienia między 4 kategoriami. Więc dziękuję!
ryyker
11
Wydaje mi Thay exemple 3 nie jest reentrant: jeśli obsługi sygnału, przerywając po t = *x, połączenia swap(), a następnie tzostaną nadpisane, co prowadzi do nieoczekiwanych rezultatów.
rom1v
1
@ SandBag_1996, rozważmy wezwanie do swap(5, 6)przerwania przez swap(1, 2). Po t=*x, s=t_originali t=5. Teraz po przerwie s=5i t=1. Jednak zanim drugi swappowróci, przywróci kontekst, tworząc t=s=5. Teraz wracamy do pierwszego swapz t=5 and s=t_originali kontynuujemy po t=*x. Tak więc funkcja wydaje się być ponownie wprowadzana. Pamiętaj, że każde wywołanie otrzymuje własną kopię salokacji na stosie.
urnonav
4
@ SandBag_1996 Zakłada się, że jeśli funkcja zostanie przerwana (w dowolnym momencie), należy ją tylko wywołać ponownie i czekamy, aż zakończy się, przed kontynuowaniem pierwotnego wywołania. Jeśli dzieje się cokolwiek innego, to w zasadzie jest to wielowątkowość, a ta funkcja nie jest bezpieczna dla wątków. Załóżmy, że funkcja robi ABCD, akceptujemy tylko takie rzeczy, jak AB_ABCD_CD lub A_ABCD_BCD, a nawet A__AB_ABCD_CD__BCD. Jak możesz sprawdzić, przykład 3 działałby dobrze przy tych założeniach, więc jest ponownie wprowadzany. Mam nadzieję że to pomoże.
MiniQuark
1
@ SandBag_1996, mutex faktycznie uniemożliwiłby ponowne wejście. Pierwsze wywołanie blokuje muteks. Nadchodzi drugie wywołanie - impas.
urnonav
56

To zależy od definicji. Na przykład Qt używa następującego:

  • Funkcja bezpieczna dla wątków * może być wywoływana jednocześnie z wielu wątków, nawet jeśli wywołania używają danych współużytkowanych, ponieważ wszystkie odwołania do danych udostępnionych są serializowane.

  • ZA Reentrant funkcja może być również nazywane równocześnie z wielu wątków, ale tylko wtedy, gdy każde wywołanie używa swoich własnych danych.

Stąd a funkcja bezpieczna wątków jest zawsze ponowna, ale funkcja ponownie wprowadzana nie zawsze jest bezpieczna dla wątków.

W związku z tym mówi się o klasie ponownie wprowadzana, jeśli jej funkcje składowe mogą być bezpiecznie wywoływane z wielu wątków, o ile każdy wątek używa innej instancji klasy. Klasa jest bezpieczna dla wątków, jeśli jej funkcje składowe można bezpiecznie wywoływać z wielu wątków, nawet jeśli wszystkie wątki używają tego samego wystąpienia klasy.

ale także ostrzegają:

Uwaga: Terminologia w dziedzinie wielowątkowości nie jest całkowicie ustandaryzowana. POSIX używa definicji reentrant i thread-safe, które są nieco inne dla jego C API. Używając innych obiektowych bibliotek klas C ++ z Qt, upewnij się, że definicje są zrozumiałe.

Georg Schölly
źródło
2
Ta definicja ponownego wejścia jest zbyt mocna.
qweruiop
Funkcja jest zarówno ponownie wprowadzana, jak i bezpieczna dla wątków, jeśli nie używa żadnych zmiennych globalnych / statycznych. Wątek - bezpieczny: jeśli wiele wątków uruchamia twoją funkcję w tym samym czasie, czy istnieje wyścig? Jeśli używasz global var, użyj blokady, aby go chronić. więc jest bezpieczny dla wątków. reentrant: jeśli sygnał pojawi się podczas wykonywania funkcji i ponownie wywołaj swoją funkcję w sygnale, czy jest to bezpieczne ??? w takim przypadku nie ma wielu wątków. Najlepiej nie używać żadnych statycznych / globalnych
zmiennych do ponownego wejścia