Threadsafe vs reentrante

Respuestas:

42

Las funciones reentrantes no dependen de las variables globales que están expuestas en los encabezados de la biblioteca de C ... tome strtok () vs strtok_r () por ejemplo en C.

Algunas funciones necesitan un lugar para almacenar un 'trabajo en progreso', las funciones reentrantes le permiten especificar este puntero dentro del propio almacenamiento del hilo, no en un global. Dado que este almacenamiento es exclusivo de la función de llamada, se puede interrumpir y volver a ingresar (reentrante) y dado que en la mayoría de los casos no se requiere la exclusión mutua más allá de lo que implementa la función para que esto funcione, a menudo se considera que son hilo seguro . Sin embargo, esto no está garantizado por definición.

errno, sin embargo, es un caso ligeramente diferente en los sistemas POSIX (y tiende a ser el bicho raro en cualquier explicación de cómo funciona todo esto) :)

En resumen, reentrante a menudo significa seguro para subprocesos (como en "use la versión reentrante de esa función si está usando subprocesos"), pero seguro para subprocesos no siempre significa reentrante (o al revés). Cuando busca seguridad en subprocesos, la concurrencia es lo que debe pensar. Si tiene que proporcionar un medio de bloqueo y exclusión mutua para usar una función, entonces la función no es inherentemente segura para subprocesos.

Pero no es necesario examinar todas las funciones. malloc()no tiene necesidad de ser reentrante, no depende de nada fuera del alcance del punto de entrada para un hilo dado (y es en sí mismo seguro para hilos).

Las funciones que devuelven valores asignados estáticamente no son seguras para subprocesos sin el uso de un mutex, futex u otro mecanismo de bloqueo atómico. Sin embargo, no necesitan volver a entrar si no van a ser interrumpidos.

es decir:

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

Entonces, como puede ver, tener múltiples subprocesos usando eso sin algún tipo de bloqueo sería un desastre ... pero no tiene ningún propósito volver a entrar. Te encontrarás con eso cuando la memoria asignada dinámicamente sea tabú en alguna plataforma integrada.

En la programación puramente funcional, reentrante a menudo no implica que sea seguro para subprocesos, dependería del comportamiento de funciones definidas o anónimas pasadas al punto de entrada de la función, recursividad, etc.

Una mejor manera de poner 'seguro para subprocesos' es segura para el acceso concurrente , lo que ilustra mejor la necesidad.

Tim Post
fuente
2
Reentrant no implica que sea seguro para subprocesos. Las funciones puras implican seguridad en los hilos.
Julio Guerra
Gran respuesta Tim. Solo para aclarar, mi entendimiento de su "a menudo" es que seguro para subprocesos no implica reentrante, pero también reentrante no implica seguro para subprocesos. ¿Podría encontrar un ejemplo de una función reentrante que no sea segura para subprocesos?
Riccardo
@ Tim Post "En resumen, reentrante a menudo significa seguro para subprocesos (como en" use la versión reentrante de esa función si está usando subprocesos "), pero seguro para subprocesos no siempre significa reentrante". qt dice lo contrario: "Por lo tanto, una función segura para subprocesos siempre es reentrante, pero una función reentrante no siempre es segura para subprocesos".
4pie0
y wikipedia dice algo más: "Esta definición de reentrada difiere de la de seguridad de subprocesos en entornos de subprocesos múltiples. Una subrutina reentrante puede lograr seguridad de subprocesos, [1] pero ser reentrante por sí solo podría no ser suficiente para ser seguro para subprocesos" en todas las situaciones. Por el contrario, el código seguro para subprocesos no tiene que ser necesariamente reentrante (...) "
4pie0
@Riccardo: Las funciones sincronizadas a través de variables volátiles pero no barreras de memoria completas para su uso con manejadores de señales / interrupciones suelen ser reentrantes pero seguras para subprocesos.
Doynax
77

TL; DR: una función puede ser reentrante, segura para subprocesos, ambas o ninguna.

Vale la pena leer los artículos de Wikipedia sobre seguridad de subprocesos y reentrada . Aquí hay algunas citas:

Una función es segura para subprocesos si:

solo manipula las estructuras de datos compartidas de una manera que garantiza la ejecución segura de varios subprocesos al mismo tiempo.

Una función es reentrante si:

se puede interrumpir en cualquier momento durante su ejecución y luego volver a llamar de forma segura ("reingresar") antes de que sus invocaciones anteriores completen la ejecución.

Como ejemplos de posible reentrada, Wikipedia da el ejemplo de una función diseñada para ser llamada por interrupciones del sistema: supongamos que ya se está ejecutando cuando ocurre otra interrupción. Pero no crea que está seguro solo porque no codifica con interrupciones del sistema: puede tener problemas de reentrada en un programa de un solo subproceso si usa devoluciones de llamada o funciones recursivas.

La clave para evitar confusiones es que reentrante se refiere a la ejecución de un solo hilo. Es un concepto de la época en que no existían sistemas operativos multitarea.

Ejemplos

(Ligeramente modificado de los artículos de Wikipedia)

Ejemplo 1: no seguro para subprocesos, no reentrante

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

Ejemplo 2: seguro para subprocesos, no reentrante

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

Example 3: not thread-safe, 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;
}

Example 4: thread-safe, reentrant

/* 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
fuente
10
I know I'm not supposed to comment just to say thanks, but this is one of the best illustrations laying out the differences between re-entrant and thread safe functions. In particular you have used very concise clear terms, and chose a great example function to distinguish between the 4 categories. So, Thanks!
ryyker
11
It seems to me thay exemple 3 is not reentrant: if a signal handler, interrupting after t = *x, calls swap(), then t will be overridden, leading to unexpected results.
rom1v
1
@SandBag_1996, let's consider a call to swap(5, 6) being interrupted by a swap(1, 2). After t=*x, s=t_original and t=5. Now, after the interruption, s=5 and t=1. However, before the second swap returns it will restore context, making t=s=5. Now, we go back to the first swap with t=5 and s=t_original and continue after t=*x. So, the function does appear to be re-entrant. Remember that every call gets its own copy of s allocated on stack.
urnonav
4
@SandBag_1996 The assumption is that if the function gets interrupted (at any point), it's only to be called again, and we wait until it completes before continuing the original call. If anything else happens, then it's basically multithreading, and this function is not thread-safe. Suppose the function does ABCD, we only accept things like AB_ABCD_CD, or A_ABCD_BCD, or even A__AB_ABCD_CD__BCD. As you can check, example 3 would work fine under these assumptions, so it is reentrant. Hope this helps.
MiniQuark
1
@SandBag_1996, mutex would actually make it non-reentrant. First invocation locks mutex. In comes second invocation - deadlock.
urnonav
56

It depends on the definition. For example Qt uses the following:

  • A thread-safe* function can be called simultaneously from multiple threads, even when the invocations use shared data, because all references to the shared data are serialized.

  • A reentrant function can also be called simultaneously from multiple threads, but only if each invocation uses its own data.

Hence, a thread-safe function is always reentrant, but a reentrant function is not always thread-safe.

By extension, a class is said to be reentrant if its member functions can be called safely from multiple threads, as long as each thread uses a different instance of the class. The class is thread-safe if its member functions can be called safely from multiple threads, even if all the threads use the same instance of the class.

but they also caution:

Note: Terminology in the multithreading domain isn't entirely standardized. POSIX uses definitions of reentrant and thread-safe that are somewhat different for its C APIs. When using other object-oriented C++ class libraries with Qt, be sure the definitions are understood.

Georg Schölly
fuente
2
This definition of reentrant is too strong.
qweruiop
A function is both reentrant and thread-safe if it doesn't use any global /static var. Thread - safe: when many threads runs your function at the same time, is there any race?? If you use global var, use lock to protect it. so it is thread-safe. reentrant: if a signal occurs during your function execution, and call your function in the signal again, is it safe??? in such case, there is no multiple threads. It's best that your don't use any static/global var to make it reentrant, or like in example 3.
keniee van