Threadsafe vs. Reentrant

Lesezeit: 8 Minuten

Benutzeravatar von Alphaneo
Alphaneo

Kürzlich habe ich eine Frage mit dem Titel „Ist malloc threadsicher?“ gestellt und darin gefragt: „Ist malloc wiedereintrittsfähig?“

Ich hatte den Eindruck, dass alle Wiedereinsteiger Thread-sicher sind.

Ist diese Annahme falsch?

Benutzeravatar von MiniQuark
MiniQuark

TL;DR: Eine Funktion kann reentrant, threadsicher, beides oder keines sein.

Die Wikipedia-Artikel für Thread-Sicherheit und Wiedereintritt sind sehr lesenswert. Hier ein paar Zitate:

Eine Funktion ist Thread-sicher wenn:

Es manipuliert gemeinsam genutzte Datenstrukturen nur so, dass eine sichere Ausführung durch mehrere Threads gleichzeitig gewährleistet ist.

Eine Funktion ist wiedereintretend wenn:

es kann an jedem Punkt während seiner Ausführung unterbrochen und dann sicher erneut aufgerufen (“erneut eingegeben”) werden, bevor seine vorherigen Aufrufe die Ausführung abschließen.

Als Beispiel für einen möglichen Wiedereintritt gibt die Wikipedia das Beispiel einer Funktion, die dazu bestimmt ist, von Systemunterbrechungen aufgerufen zu werden: Angenommen, sie läuft bereits, wenn eine andere Unterbrechung auftritt. Aber glauben Sie nicht, dass Sie sicher sind, nur weil Sie nicht mit System-Interrupts programmieren: Sie können in einem Single-Thread-Programm Wiedereintrittsprobleme haben, wenn Sie Callbacks oder rekursive Funktionen verwenden.

Der Schlüssel zur Vermeidung von Verwirrung besteht darin, dass reentrant sich auf nur einen Thread bezieht, der ausgeführt wird. Es ist ein Konzept aus der Zeit, als es noch keine Multitasking-Betriebssysteme gab.

Beispiele

(Leicht modifiziert aus den Wikipedia-Artikeln)

Beispiel 1: nicht Thread-sicher, nicht reentrant

/* 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;
}

Beispiel 2: Thread-sicher, nicht reentrant

/* 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;
}

Beispiel 3: nicht Thread-sicher, reentrant

/* 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;
}

Beispiel 4: Thread-sicher, ablaufinvariant

/* 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;
}

  • Ich weiß, dass ich nicht kommentieren soll, nur um mich zu bedanken, aber dies ist eine der besten Illustrationen, die die Unterschiede zwischen reentranten und Thread-sicheren Funktionen darlegt. Insbesondere haben Sie sehr prägnante, klare Begriffe verwendet und eine großartige Beispielfunktion gewählt, um zwischen den 4 Kategorien zu unterscheiden. So danke!

    – ryker

    15. Juli 2017 um 0:01 Uhr


  • Es scheint mir, dass Beispiel 3 nicht wiedereintrittsfähig ist: Wenn ein Signalhandler, unterbrechen danach t = *xAnrufe swap()dann t wird überschrieben, was zu unerwarteten Ergebnissen führt.

    – rom1v

    6. Januar 2018 um 23:29 Uhr

  • @SandBag_1996, betrachten wir einen Anruf bei swap(5, 6) unterbrochen von a swap(1, 2). Nach t=*x, s=t_original und t=5. Jetzt, nach der Unterbrechung, s=5 und t=1. Allerdings vor dem zweiten swap kehrt zurück, es wird den Kontext wiederherstellen, machen t=s=5. Jetzt gehen wir zurück zum ersten swap mit t=5 and s=t_original und danach weiter t=*x. Die Funktion scheint also wiedereintrittsfähig zu sein. Denken Sie daran, dass jeder Anruf eine eigene Kopie von erhält s auf Stapel zugewiesen.

    – Urnav

    28. Februar 2019 um 2:57 Uhr


  • @SandBag_1996 Die Annahme ist, dass, wenn die Funktion (zu irgendeinem Zeitpunkt) unterbrochen wird, sie nur erneut aufgerufen werden muss, und wir warten, bis sie abgeschlossen ist, bevor wir den ursprünglichen Aufruf fortsetzen. Wenn irgendetwas anderes passiert, dann ist es im Grunde Multithreading, und diese Funktion ist es nicht Thread-sicher. Angenommen, die Funktion macht ABCD, wir akzeptieren nur Dinge wie AB_ABCD_CD oder A_ABCD_BCD oder sogar A__AB_ABCD_CD__BCD. Wie Sie überprüfen können, würde Beispiel 3 unter diesen Annahmen gut funktionieren, also ist es reentrant. Hoffe das hilft.

    – MiniQuark

    28. Februar 2019 um 9:56 Uhr

  • @SandBag_1996, Mutex würde es tatsächlich nicht wiedereintretend machen. Erster Aufruf sperrt Mutex. Dann kommt der zweite Aufruf – Deadlock.

    – Urnav

    28. Februar 2019 um 19:15 Uhr

Benutzeravatar von Georg Schölly
Georg Scholly

Es kommt auf die Definition an. Zum Beispiel Qt verwendet folgende:

  • Eine Thread-sichere* Funktion kann gleichzeitig von mehreren Threads aufgerufen werden, selbst wenn die Aufrufe gemeinsam genutzte Daten verwenden, da alle Verweise auf die gemeinsam genutzten Daten serialisiert werden.

  • EIN wiedereintretend -Funktion kann auch gleichzeitig von mehreren Threads aufgerufen werden, aber nur, wenn jeder Aufruf seine eigenen Daten verwendet.

Daher ein Thread-sicher Funktion ist immer reentrant, aber a wiedereintretend Die Funktion ist nicht immer Thread-sicher.

Im weiteren Sinne soll eine Klasse sein wiedereintretend wenn seine Member-Funktionen sicher von mehreren Threads aufgerufen werden können, solange jeder Thread eine andere Instanz der Klasse verwendet. Die Klasse ist Thread-sicher wenn seine Member-Funktionen sicher von mehreren Threads aufgerufen werden können, selbst wenn alle Threads dieselbe Instanz der Klasse verwenden.

aber sie warnen auch:

Notiz: Die Terminologie in der Multithreading-Domäne ist nicht vollständig standardisiert. POSIX verwendet für seine C-APIs etwas andere Definitionen von reentrant und thread-safe. Wenn Sie andere objektorientierte C++-Klassenbibliotheken mit Qt verwenden, stellen Sie sicher, dass Sie die Definitionen verstanden haben.

  • Diese Definition von Reentrant ist zu stark.

    – qweruiop

    25. Oktober 2014 um 2:33 Uhr

  • Eine Funktion ist sowohl ablaufinvariant als auch threadsicher, wenn sie keine globale /statische Variable verwendet. Thread – sicher: Wenn viele Threads gleichzeitig Ihre Funktion ausführen, gibt es ein Rennen? Wenn Sie globale Variablen verwenden, verwenden Sie lock, um sie zu schützen. es ist also Thread-sicher. Reentrant: Wenn während der Ausführung Ihrer Funktion ein Signal auftritt und Ihre Funktion im Signal erneut aufgerufen wird, ist es sicher??? in einem solchen Fall gibt es keine mehreren Threads. Es ist am besten, dass Sie keine statische/globale Variable verwenden, um sie wiedereintrittsfähig zu machen, oder wie in Beispiel 3.

    – Keniee-Van

    3. Januar 2018 um 7:31 Uhr

Benutzeravatar von Tim Post
Tim Post

Wiedereintretende Funktionen verlassen sich nicht auf globale Variablen, die in den Headern der C-Bibliothek verfügbar gemacht werden. Nehmen Sie zum Beispiel strtok() vs strtok_r() in C.

Einige Funktionen benötigen einen Ort zum Speichern eines ‘work in progress’ . Reentrant-Funktionen ermöglichen es Ihnen, diesen Zeiger im eigenen Speicher des Threads anzugeben, nicht in einem globalen. Da diese Speicherung ausschließlich der Anruffunktion vorbehalten ist, kann sie auch unterbrochen werden neu eingegeben (wiedereintretend) und da in den meisten Fällen ein gegenseitiger Ausschluss über das hinaus, was die Funktion implementiert, nicht erforderlich ist, damit dies funktioniert, werden sie oft als solche angesehen Thread sicher. Dies ist jedoch nicht per Definition garantiert.

errno ist jedoch ein etwas anderer Fall auf POSIX-Systemen (und neigt dazu, bei jeder Erklärung, wie das alles funktioniert, ein Sonderling zu sein) 🙂

Kurz gesagt, Wiedereinsteiger häufig bedeutet Thread-sicher (wie in “verwenden Sie die ablaufinvariante Version dieser Funktion, wenn Sie Threads verwenden”), aber Thread-sicher bedeutet nicht immer wiedereintritt (oder umgekehrt). Wenn Sie sich die Thread-Sicherheit ansehen, Gleichzeitigkeit ist das, woran Sie denken müssen. Wenn Sie eine Funktion zum Sperren und gegenseitigen Ausschließen bereitstellen müssen, um eine Funktion zu verwenden, ist die Funktion nicht von Natur aus Thread-sicher.

Es müssen aber auch nicht alle Funktionen untersucht werden. malloc() muss nicht wiedereintrittsfähig sein, es hängt von nichts außerhalb des Geltungsbereichs des Einstiegspunkts für einen bestimmten Thread ab (und ist selbst Thread-sicher).

Funktionen, die statisch zugewiesene Werte zurückgeben, sind nicht Thread-sicher ohne die Verwendung eines Mutex-, Futex- oder anderen atomaren Verriegelungsmechanismus. Sie müssen jedoch nicht wiedereintrittsfähig sein, wenn sie nicht unterbrochen werden sollen.

dh:

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;
}

Wie Sie also sehen können, wäre es eine Katastrophe, mehrere Threads ohne irgendeine Art von Sperrung zu verwenden. Aber es hat keinen Zweck, wiedereintrittsfähig zu sein. Sie werden darauf stoßen, wenn dynamisch zugewiesener Speicher auf einigen eingebetteten Plattformen tabu ist.

Bei rein funktionaler Programmierung oft reentrant nicht Thread-sicher implizieren, würde dies vom Verhalten definierter oder anonymer Funktionen abhängen, die an den Einstiegspunkt der Funktion, Rekursion usw. übergeben werden.

Ein besserer Weg, um “threadsicher” zu setzen, ist sicher für gleichzeitigen Zugriff was die Notwendigkeit besser verdeutlicht.

  • Reentrant bedeutet nicht Thread-sicher. Reine Funktionen implizieren Thread-Sicherheit.

    – Julio Guerra

    30. Mai 2013 um 13:36 Uhr

  • Tolle Antwort Tim. Nur zur Verdeutlichung, mein Verständnis von Ihrem “oft” ist, dass Thread-Safe nicht Reentrant impliziert, aber auch Reentrant nicht Thread-Safe impliziert. Können Sie ein Beispiel für eine reentrante Funktion finden, die ist nicht Thread-sicher?

    – Riccardo

    9. Juni 2014 um 8:28 Uhr

  • @ Tim Post “Kurz gesagt, reentrant bedeutet oft threadsicher (wie in “verwenden Sie die reentrante Version dieser Funktion, wenn Sie Threads verwenden”), aber threadsicher bedeutet nicht immer reentrant.” Quart sagt Gegenteil: “Daher ist eine Thread-sichere Funktion immer wiedereintrittsfähig, aber eine wiedereintrittsfähige Funktion ist nicht immer Thread-sicher.”

    – 4pie0

    11. Februar 2015 um 10:11 Uhr

  • und Wikipedia sagt noch etwas anderes: “Diese Definition von Reentrancy unterscheidet sich von der Thread-Sicherheit in Umgebungen mit mehreren Threads. Eine reentrante Subroutine kann Thread-Sicherheit erreichen,[1] Reentrant allein reicht jedoch möglicherweise nicht aus, um in allen Situationen Thread-sicher zu sein. Umgekehrt muss Thread-sicherer Code nicht unbedingt ablaufinvariant sein (…)”

    – 4pie0

    11. Februar 2015 um 10:28 Uhr


  • @Riccardo: Funktionen, die über flüchtige Variablen synchronisiert werden, aber keine vollen Speicherbarrieren für die Verwendung mit Signal-/Interrupt-Handlern, sind normalerweise wiedereintrittsfähig, aber Thread-sicher.

    – doynax

    9. Oktober 2015 um 14:57 Uhr

1420730cookie-checkThreadsafe vs. Reentrant

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy