Threadsafe vs re-entrant

Hace poco, hice una pregunta, con el título “¿Está seguro el hilo de Malloc?” , y dentro de eso pregunté: “¿Malloc está reingresando?”

Tenía la impresión de que todos los reentrantes son seguros para subprocesos.

¿Esta suposición es incorrecta?

Las funciones de reentrada no se basan en las variables globales que están expuestas en los encabezados de la biblioteca C. tomen strtok () frente a strtok_r (), por ejemplo, en C.

Algunas funciones necesitan un lugar para almacenar un ‘trabajo en progreso’, las funciones de reentrada le permiten especificar este puntero dentro del propio almacenamiento del subproceso, no de forma global. Como este almacenamiento es exclusivo de la función de llamada, se puede interrumpir y reingresar (volver a ingresar ) y dado que en la mayoría de los casos no se requiere la exclusión mutua más allá de lo que la función implementa para que esto funcione, a menudo se considera 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 hilo seguro (como en “usar la versión reentrante de esa función si estás usando subprocesos”), pero el hilo seguro no siempre significa volver a entrar (o al revés). Cuando buscas seguridad en hilos, la concurrencia es en lo que debes pensar. Si tiene que proporcionar un medio de locking y exclusión mutua para usar una función, entonces la función no es intrínsecamente segura para subprocesos.

Pero, no todas las funciones necesitan ser examinadas para ninguno. malloc() no necesita ser reentratado, no depende de nada fuera del scope del punto de entrada para un hilo dado (y es en sí mismo seguro para subprocesos).

Las funciones que devuelven valores asignados estáticamente no son seguras para hilos sin el uso de un mutex, futex u otro mecanismo de locking atómico. Sin embargo, no necesitan ser reentrantes 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 varios hilos que usen eso sin algún tipo de locking sería un desastre … pero no tiene ningún propósito reingresar. Se encontrará con eso cuando la memoria asignada dinámicamente es tabú en alguna plataforma incorporada.

En la progtwigción puramente funcional, reentrante a menudo no implica seguridad de subprocesos, dependería del comportamiento de funciones definidas o anónimas pasadas al punto de entrada de la función, recursión, etc.

Una mejor manera de poner ‘hilo seguro’ es seguro para el acceso simultáneo , que ilustra mejor la necesidad.

Depende de la definición. Por ejemplo, Qt usa lo siguiente:

  • Una función thread-safe * se puede invocar simultáneamente desde varios subprocesos, incluso cuando las invocaciones usan datos compartidos, porque todas las referencias a los datos compartidos se serializan.

  • También se puede invocar simultáneamente una función de reentrada desde varios subprocesos, pero solo si cada invocación usa sus propios datos.

Por lo tanto, una función de seguridad de subprocesos siempre es reentrante, pero una función de reentrada no siempre es segura para subprocesos.

Por extensión, se dice que una clase es reentrante si sus funciones miembro pueden llamarse de forma segura desde varios subprocesos, siempre que cada subproceso use una instancia diferente de la clase. La clase es segura para subprocesos si sus funciones miembro pueden llamarse de forma segura desde varios subprocesos, incluso si todos los subprocesos utilizan la misma instancia de la clase.

pero también advierten:

Nota: La terminología en el dominio multiproceso no está completamente estandarizada. POSIX usa definiciones de reentrantes y subprocesos que son algo diferentes para sus API de C. Al usar otras librerías de clases C ++ orientadas a objetos con Qt, asegúrese de que se entiendan las definiciones.

TL; DR: una función puede ser reentrante, hilo seguro, ambos o ninguno.

Merece la pena leer los artículos de Wikipedia sobre seguridad y reentrada de hilos . 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 por varios hilos 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 (“volver a ingresar”) antes de que las 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 piense que está seguro solo porque no codifica las interrupciones del sistema: puede tener problemas de reentrada en un progtwig de un solo subproceso si usa devoluciones de llamada o funciones recursivas.

La clave para evitar confusiones es que la reentrada se refiere a la ejecución de solo un 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 es 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; } 

Ejemplo 3: no es seguro para subprocesos, reentrante

 /* We save the global state in a local variable and we restre 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; } 

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