Czym dokładnie jest funkcja reentrant?

Najbardziej z na czasy , definicja reentrance jest cytowana zWikipedii :

Program komputerowy lub rutyna jest opisywany jako reentrant, jeśli może być bezpiecznie wezwany ponownie przed jego poprzednie wywołanie zostało zakończone (tzn. można go bezpiecznie wykonać jednocześnie). Być reentrant, a program komputerowy lub rutyna:

  1. Nie może zawierać statycznych (lub globalnych) niestały data.
  2. Nie wolno zwracać adresu do statyczna (lub globalna) stała data.
  3. musi działać tylko na dostarczonych danych do niego przez rozmówcę.
  4. Nie wolno polegać na blokadach do Singletona zasoby.
  5. Nie może modyfikować własnego kodu (chyba wykonanie we własnym unikalnym wątku storage)
  6. Nie może wywoływać komputera nie-reentrantowego programy lub procedury.

Jak jest bezpiecznie zdefiniowany?

Jeśli program może być bezpiecznie wykonywany jednocześnie, czy to zawsze oznacza, że jest reentrant?

Jaki dokładnie jest wspólny wątek pomiędzy wymienionymi sześcioma punktami, o którym powinienem pamiętać podczas sprawdzania kodu pod kątem możliwości reentrant?

Również,

  1. czy wszystkie funkcje rekurencyjne są reentrujące?
  2. czy wszystkie funkcje bezpieczne dla wątków są obecne?
  3. są wszystkimi funkcjami rekurencyjnymi i bezpiecznymi dla wątków reentrant?

Pisząc to pytanie, jedno przychodzi mi do głowy: Czy terminy takie jak reentrance i thread safety są w ogóle bezwzględne, tzn. czy mają stałe konkretne definicje? Bo jeśli tak nie jest, to pytanie nie jest zbyt znaczące.

Author: Community, 2010-05-10

7 answers

1. Jak definiowane jest bezpiecznie?

Semantycznie. W tym przypadku nie jest to termin ściśle zdefiniowany. 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 reentrant?

Nie.

Na przykład, miejmy 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 mutex:

void bar()
{
    foo(nullptr);
}

At na pierwszy rzut oka wszystko wydaje się ok... ale czekaj:

int main()
{
    foo(bar);
    return 0;
}

Jeśli blokada w mutex nie jest rekurencyjna, to oto, co się stanie w głównym wątku:

  1. main wywoła foo.
  2. foo zdobędzie zamek.
  3. foo wywoła bar, które wywoła foo.
  4. drugi foo będzie próbował zdobyć zamek, zawieść i poczekać, aż zostanie zwolniony.
  5. impas.
  6. UPS ...
Ok, oszukiwałem, używając tego oddzwaniania. Ale łatwo wyobrazić sobie bardziej złożone fragmenty kodu o podobnym działaniu.

3. Jaki dokładnie jest wspólny wątek między wymienionymi sześcioma punktami, o którym powinienem pamiętać, sprawdzając mój kod pod kątem możliwości reentrant?

Możesz wyczuć problem, jeśli twoja funkcja ma/daje dostęp do modyfikowalnego trwałego zasobu, lub ma/daje dostęp do funkcji, która pachnie .

(Ok, 99% naszego kodu powinno śmierdzieć, więc ... Zobacz ostatni rozdział do zajmij się tym....)

Więc, studiując Twój kod, jeden z tych punktów powinien cię ostrzec:

    Funkcja posiada stan (np. dostęp do zmiennej globalnej, a nawet do zmiennej klasy).]}
  1. ta funkcja może być wywołana przez wiele wątków lub może pojawić się dwa razy na stosie podczas wykonywania procesu(tzn. funkcja może wywołać samą siebie, bezpośrednio lub pośrednio). Funkcja przyjmująca wywołania zwrotne jako parametry zapach dużo.

Zauważ, że nie-reentrancja jest wirusowa : funkcja, która może wywołać możliwą funkcję nie-reentrantową, nie może być uważana za reentrantową.

Zauważ również, że metody C++ pachną ponieważ mają dostęp do this, więc powinieneś przestudiować kod, aby mieć pewność, że nie mają zabawnej interakcji.

4.1. Czy wszystkie funkcje rekurencyjne są recentrujące?

Nie.

W przypadkach wielowątkowych, funkcja rekurencyjna uzyskująca dostęp do współdzielonych zasobów może być wywoływana przez wiele wątków jednocześnie moment, powodując złe / uszkodzone dane.

W pojedynczych przypadkach funkcja rekurencyjna może używać funkcji nie-reentrantowej (jak infamous strtok) lub używać danych globalnych bez obsługi faktu, że dane są już w użyciu. Więc twoja funkcja jest rekurencyjna, ponieważ wywołuje się bezpośrednio lub pośrednio, ale nadal może być rekurencyjno-niebezpieczna .

4.2. Czy wszystkie funkcje thread-safe działają ponownie?

W powyższym przykładzie pokazałem, jak pozornie threadsafe funkcja nie była reentrantowa. Ok oszukiwałem z powodu parametru callback. Ale jest wiele sposobów na zablokowanie wątku poprzez dwukrotne uzyskanie blokady nie rekurencyjnej.

4.3. Czy wszystkie funkcje rekurencyjne i bezpieczne dla wątków są recentrowane?

Powiedziałbym "tak", jeśli przez " rekurencyjny "masz na myśli"rekurencyjny-Bezpieczny".

Jeśli możesz zagwarantować, że funkcja może być wywoływana jednocześnie przez wiele wątków i może wywoływać samą siebie, bezpośrednio lub pośrednio, bez problemów, to to jest reentrant.

Problem polega na ocenie tej gwarancji ... ^ _ ^

5. Czy terminy takie jak reentrance i thread safety są w ogóle bezwzględne, tzn. czy mają stałe konkretne definicje?

Wierzę, że mają, ale wtedy ocena funkcji jest bezpieczna dla wątku lub reentrant może być trudna. Dlatego użyłem terminu smell powyżej: można znaleźć funkcję nie jest reentrant, ale może być trudno mieć pewność, że złożony fragment kodu jest reentrant

6. Przykład

Załóżmy, że masz obiekt, z jedną metodą, która wymaga użycia zasobów:

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 ta funkcja jest wywoływana rekurencyjnie (tzn. ta funkcja wywołuje się bezpośrednio lub pośrednio), kod prawdopodobnie ulegnie awarii, ponieważ this->p zostanie usunięty pod koniec ostatniego wywołania i nadal prawdopodobnie będzie używany przed końcem pierwszego wywołania.

Zatem ten kod nie jest Bezpieczny rekurencyjnie.

Moglibyśmy użyć licznik odniesienia do poprawienia tego:

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ę rekurencyjny-bezpieczny... ale nadal nie jest reentrantowy z powodu problemów z wielowątkowością: musimy być pewni, że modyfikacje c i p będą wykonywane atomicznie, używając rekurencyjnego mutex (nie wszystkie mutexy są rekurencyjne): {[28]]}

#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, że lots of code jest samo w sobie reentrant, łącznie z użyciem p.

A powyższy kod nie jest nawet zdalnie wyjątek-Bezpieczny , ale to już inna historia... ^ _ ^

7. Hej 99% naszego kodu nie jest reentrant!

To prawda dla spaghetti code. Ale jeśli partycjonujesz poprawnie kod, unikniesz problemów z reentrancją.

7.1. Upewnij się, że wszystkie funkcje nie mają stanu

Muszą używać tylko parametrów, własnych zmiennych lokalnych, innych funkcji bez stanu i zwracać kopie danych, jeśli w ogóle powrócą.

7.2. Upewnij się, że Twój obiekt jest "bezpieczny rekurencyjnie"

Metoda obiektowa ma dostęp do this, więc dzieli stan ze wszystkimi metodami tej samej instancji obiektu.

Więc upewnij się, że obiekt może być użyty w jednym punkcie stosu (tzn. wywołanie metody A), a następnie w innym punkcie (tzn. wywołanie metody B), bez uszkodzenia całego obiektu. Zaprojektuj swój obiekt, aby upewnić się, że po wyjściu z metody obiekt jest stabilny i poprawny (bez zwisających wskaźników, bez sprzecznych zmiennych członkowskich, itd.).

7.3. Upewnij się, że wszystkie obiekty są prawidłowo zamknięte

Nikt inny nie powinien mieć dostępu do swoich wewnętrznych danych:

    // 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 zwracanie referencji const może być niebezpieczne, jeśli użycie pobiera adres danych, ponieważ inna część kodu może ją zmodyfikować bez podawania kodu zawierającego referencję const.

7.4. Upewnij się, że użytkownik wie, że obiekt nie jest bezpieczny dla wątków]}

W związku z tym użytkownik jest odpowiedzialny za używanie muteksy do używania obiektu współdzielonego między wątkami.

Jeśli użytkownik chce udostępnić plik std::string pomiędzy dwoma wątkami, musi chronić swój dostęp za pomocą współbieżnych prymitywów.]}

7.5. Upewnij się, że kod thread-safe jest rekurencyjny-Bezpieczny

Oznacza to używanie rekurencyjnych muteksów, jeśli uważasz, że ten sam zasób może być użyty dwa razy przez ten sam wątek.

 167
Author: paercebal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-24 18:42:06

" bezpiecznie "jest definiowane dokładnie tak, jak nakazuje zdrowy rozsądek - oznacza"robienie swoich rzeczy poprawnie bez ingerencji w inne rzeczy". Sześć przytoczonych przez Ciebie punktów jasno wyraża wymagania, które pozwolą ci to osiągnąć.

Odpowiedzi na twoje 3 pytania to 3× "nie".


czy wszystkie funkcje rekurencyjne są recentrujące?

Nie!

Dwa równoczesne wywołania funkcji rekurencyjnej mogą się łatwo przekręcić, jeśli mają dostęp do tego samego na przykład dane globalne/statyczne.


czy wszystkie funkcje thread-safe działają ponownie?

Nie!

Funkcja jest bezpieczna dla wątku, jeśli nie działa, jeśli jest wywoływana jednocześnie. Ale można to osiągnąć np. poprzez użycie mutex do zablokowania wykonania drugiego wywołania aż do zakończenia pierwszego, więc tylko jedno wywołanie działa na raz. Reentrancy oznacza wykonywanie jednocześnie bez ingerencji w inne wywołania .


są wszystkie funkcje rekurencyjne i bezpieczne dla wątków są aktywne?

Nie!

Patrz wyżej.

 20
Author: slacker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-05-09 20:39:05

Wspólny wątek:

Czy zachowanie jest dobrze zdefiniowane, jeśli rutyna jest wywoływana podczas jej przerwania?

Jeśli masz taką funkcję:

int add( int a , int b ) {
  return a + b;
}

Wtedy nie jest zależna od żadnego zewnętrznego stanu. Zachowanie jest dobrze zdefiniowane.

Jeśli masz taką funkcję:

int add_to_global( int a ) {
  return gValue += a;
}

Wynik nie jest dobrze zdefiniowany w wielu wątkach. Informacje mogą zostać utracone, jeśli czas był po prostu zły.

Najprostszą formą funkcji reentrantowej jest coś która działa wyłącznie na przekazywanych argumentach i wartościach stałych. Wszystko inne wymaga specjalnej obsługi lub, często, nie jest reentrant. I oczywiście argumenty nie mogą odwoływać się do mutable globals.

 10
Author: drawnonward,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-05-09 20:50:22

Teraz muszę rozwinąć mój poprzedni komentarz. @paercebal odpowiedź jest niepoprawna. Czy w przykładzie kodu nikt nie zauważył, że mutex, który miał być parametrem, nie został w rzeczywistości przekazany?

Kwestionuję wniosek, twierdzę: aby funkcja była bezpieczna w obecności współbieżności, musi być ponownie włączona. Dlatego współbieżne-bezpieczne (zwykle pisane thread-safe) oznacza ponowne wejście.

Ani thread safe, ani re-entrant nie mają nic do powiedzenia na temat argumentów: jesteśmy mówienie o równoczesnym wykonywaniu funkcji, które nadal może być niebezpieczne, jeśli niewłaściwe parametry są używane.

Na przykład, memcpy() jest bezpieczną dla wątku i re-entrant (Zwykle). Oczywiście nie będzie działać zgodnie z oczekiwaniami, jeśli zostanie wywołany ze wskaźnikami do tych samych celów z dwóch różnych wątków. To jest sedno definicji SGI, która nakłada na klienta obowiązek zapewnienia, że dostęp do tej samej struktury danych jest synchronizowany przez Klienta.

Ważne jest, aby zrozumieć, że w ogólne jest to nonsens aby bezpieczne działanie wątku zawierało parametry. Jeśli wykonałeś jakiekolwiek programowanie baz danych, zrozumiesz. Koncepcja tego, co jest "atomowe" i może być chronione przez mutex lub inną technikę, jest koniecznie koncepcją użytkownika: przetwarzanie transakcji w bazie danych może wymagać wielu niezakłóconych modyfikacji. Kto może powiedzieć, które z nich muszą być zsynchronizowane, ale programista klienta?

Chodzi o to, że "korupcja" nie musi być bałagan w pamięci komputera z niezerializowanymi zapisami: korupcja może nadal wystąpić, nawet jeśli wszystkie pojedyncze operacje są serializowane. Wynika z tego, że gdy pytasz, czy funkcja jest bezpieczna dla wątku, czy też nie, pytanie oznacza dla wszystkich odpowiednio rozdzielonych argumentów: użycie argumentów sprzężonych nie stanowi kontrprzykładu.

Istnieje wiele systemów programistycznych: Ocaml jest jednym z nich, i myślę, że Python również, który ma wiele nie-reentrantowego kodu, ale który wykorzystuje globalną blokadę do przeplatania acessów wątków. Systemy te nie są ponownie włączane i nie są bezpieczne dla wątków ani bezpieczne dla współbieżności, działają bezpiecznie po prostu dlatego, że zapobiegają współbieżności na całym świecie.

Dobrym przykładem jest malloc. To nie jest ponowne wejście i nie thread-bezpieczne. Dzieje się tak dlatego, że musi uzyskać dostęp do globalnego zasobu (sterty). Korzystanie z zamków nie czyni go bezpiecznym: zdecydowanie nie jest to ponowne wejście. Gdyby interfejs do malloca był poprawnie zaprojektowany, to byłoby możliwe, aby re-entrant i thread-safe:
malloc(heap*, size_t);

Teraz może być bezpieczny, ponieważ przenosi odpowiedzialność za serializację wspólnego dostępu do jednej sterty na klienta. W szczególności praca nie jest wymagana, jeśli istnieją oddzielne obiekty sterty. Jeśli używana jest wspólna sterta, klient musi serializować dostęp. Użycie blokady wewnątrz funkcja nie jest wystarczająca: wystarczy rozważyć blokowanie sterty malloc * i wtedy pojawia się sygnał i wywołuje malloc na tym samym wskaźniku: deadlock: sygnał nie może Kontynuuj, a Klient też nie może, ponieważ jest przerwany.

Ogólnie rzecz biorąc, zamki nie czynią rzeczy bezpiecznymi.. w rzeczywistości niszczą bezpieczeństwo, niewłaściwie próbując zarządzać zasobem, który jest własnością klienta. Blokowanie musi być wykonywane przez producenta obiektu, to jedyny kod, który wie, ile obiektów zostało utworzonych i jak będą one używane.

 7
Author: Yttrill,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-10 05:36:26

"wspólny wątek" (kalambur zamierzony!?) Wśród wymienionych punktów jest to, że funkcja nie może robić niczego, co miałoby wpływ na zachowanie rekurencyjnych lub równoczesnych wywołań tej samej funkcji.

Więc na przykład dane statyczne są problemem, ponieważ są własnością wszystkich wątków; jeśli jedno wywołanie modyfikuje zmienną statyczną, wszystkie wątki używają zmodyfikowanych danych wpływając tym samym na ich zachowanie. Samoodtwarzający się kod (choć rzadko spotykany, a w niektórych przypadkach uniemożliwiany) byłby problem, bo chociaż jest wiele wątków, to jest tylko jedna kopia kodu; Kod też jest niezbędny do statycznych danych.

Zasadniczo, aby zostać uczestnikiem, każdy wątek musi być w stanie korzystać z funkcji tak, jakby był jedynym użytkownikiem, a nie jest tak, jeśli jeden wątek może wpływać na zachowanie innego w sposób niedeterministyczny. Przede wszystkim polega to na tym, że każdy wątek ma oddzielne lub stałe dane, na których działa funkcja.

Wszystko, co powiedziane, punkt (1) nie jest na przykład, możesz legalnie i projektowo użyć zmiennej statycznej, aby zachować liczbę rekursji, aby chronić przed nadmierną rekursją lub profilować algorytm.

Funkcja bezpieczna dla wątku nie musi być reentrantowa; może osiągnąć bezpieczeństwo wątku poprzez szczególne zapobieganie reentrancji za pomocą blokady, a punkt (6) mówi, że taka funkcja nie jest reentrantowa. Jeśli chodzi o punkt (6), funkcja wywołująca funkcję bezpieczną dla wątku, która blokuje, nie jest Bezpieczna do użycia w rekurencji (będzie dead-lock), a zatem nie jest powiedziane, że jest reentrant, chociaż może być bezpieczny dla współbieżności i nadal będzie re-entrant w tym sensie, że wiele wątków może mieć swoje liczniki programów w takiej funkcji jednocześnie (tylko nie z zablokowanym regionem). Może to pomaga odróżnić thread-safety od reentarncy (a może dodaje do twojego zamieszania!).

 3
Author: Clifford,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-05-09 20:47:46

Odpowiedzi na twoje" również "pytania To "Nie", "Nie"i " nie". Tylko dlatego, że funkcja jest rekurencyjna i / lub bezpieczna dla wątku, nie powoduje jej ponownego włączenia.

Każda z tych funkcji może zawieść we wszystkich cytowanych punktach. (Choć nie jestem w 100% pewien punktu 5).

 1
Author: ChrisF,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-05-09 20:19:40

Terminy "Thread-safe "I" re-entrant " oznaczają tylko i dokładnie to, co mówią ich definicje. "Bezpieczny"w tym kontekście oznacza tylko{[2] } to, co cytujesz poniżej.

"Bezpieczny" tutaj z pewnością nie oznacza bezpieczny w szerszym znaczeniu, że wywołanie danej funkcji w danym kontekście nie spowoduje całkowitego wyczerpania aplikacji. Ogólnie rzecz biorąc, funkcja może niezawodnie przynieść pożądany efekt w aplikacji wielowątkowej, ale nie może kwalifikować się do ponownego włączenia lub zabezpieczenia gwintu zgodnie z definicjami. W przeciwieństwie do tego, możesz wywoływać funkcje ponownego włączenia w sposób, który przyniesie wiele niepożądanych, nieoczekiwanych i / lub nieprzewidywalnych efektów w wielowątkowej aplikacji.

Funkcja rekurencyjna może być dowolna, a Re-entrant ma mocniejszą definicję niż thread-safe, więc odpowiedzi na twoje ponumerowane pytania są wszystkie Nie.

Czytając definicję re-entranta, można ją podsumować jako funkcję, która nie zmieni niczego poza jak to się nazywa modyfikować. Ale nie powinieneś polegać tylko na podsumowaniu.

Programowanie wielowątkowe jest po prostu niezwykle trudne w ogólnym przypadku. Wiedza, która część kodu re-entrant jest tylko częścią tego wyzwania. Bezpieczeństwo nici nie jest addytywne. Zamiast kombinować funkcje re-entrant, lepiej użyć ogólnego wątku -safe wzorzec projektowy i użyj tego wzoru, aby kierować używaniem każdego wątku i udostępnianego zasoby w twoim programie.

 1
Author: Joe Soul-bringer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-05-10 01:19:13