c++ - programas - ¿Qué es exactamente una función de reentrada?
manual completo de c++ pdf (7)
Most of the times , la definición de reentrada se cita de Wikipedia :
Un programa o rutina de computadora se describe como reentrante si se puede volver a llamar de manera segura antes de que se haya completado su invocación anterior (es decir, se puede ejecutar de forma segura al mismo tiempo). Para ser reentrante, un programa o rutina de computadora:
- No debe contener datos no constantes estáticos (o globales).
- No debe devolver la dirección a datos no constantes estáticos (o globales).
- Debe trabajar solo en los datos proporcionados por la persona que llama.
- No debe confiar en los bloqueos de recursos únicos.
- No debe modificar su propio código (a menos que se ejecute en su propio almacenamiento de subprocesos único)
- No debe llamar a programas o rutinas de computadora no reentrantes.
¿Cómo se define con seguridad ?
Si un programa se puede ejecutar de manera segura al mismo tiempo , ¿significa siempre que se reentrató?
¿Cuál es exactamente el hilo común entre los seis puntos mencionados que debo tener en cuenta al verificar mi código para las capacidades de reentrada?
También,
- ¿Son todas las funciones recursivas reentrantes?
- ¿Están todas las funciones de seguridad de subprocesos reentrantes?
- ¿Están todas las funciones recursivas y seguras para subprocesos reentrantes?
Al escribir esta pregunta, una cosa viene a la mente: ¿Son los términos como reentrada y seguridad de hilo absoluto en absoluto, es decir, tienen definiciones fijas concretas? Porque, si no lo son, esta pregunta no es muy significativa.
1. ¿Cómo se define con seguridad ?
Semánticamente. En este caso, este no es un término difícil de definir. Simplemente significa "Puedes hacerlo, sin riesgo".
2. Si un programa se puede ejecutar de forma segura al mismo tiempo, ¿siempre significa que se reentrante?
No.
Por ejemplo, tengamos una función C ++ que tenga tanto un bloqueo como una devolución de llamada como parámetro:
typedef void (*callback)();
void foo(callback f)
{
lock(mutex) ;
// use the resource protected by the mutex
if(f)
f() ;
// use the resource protected by the mutex
unlock(mutex) ;
}
Otra función bien podría necesitar bloquear el mismo mutex:
void bar()
{
foo(NULL);
}
A primera vista, todo parece estar bien ... Pero espera:
int main()
{
foo(bar);
return 0 ;
}
Si el bloqueo en mutex no es recursivo, entonces esto es lo que sucederá, en el hilo principal:
-
main
llamaráfoo
-
foo
adquirirá el bloqueo -
foo
llamará albar
, que llamará afoo
- el segundo
foo
intentará adquirir el bloqueo, fallará y esperará a que se libere - Punto muerto.
- Oops ...
Ok, hice trampa, usando lo de Devolución de llamada. Pero es fácil imaginar piezas de código más complejas que tengan un efecto similar.
3. ¿Cuál es exactamente el hilo común entre los seis puntos mencionados que debo tener en cuenta al verificar mi código para las capacidades de reentrada?
Puede oler un problema si su función tiene / da acceso a un recurso persistente modificable, o tiene / da acceso a una función que huele .
( Ok, el 99% de nuestro código debería oler, luego ... Ver la última sección para manejar eso ... )
Entonces, estudiando su código, uno de esos puntos debería alertarlo:
- La función tiene un estado (es decir, accede a una variable global, o incluso a una variable miembro de la clase)
- Esta función puede ser llamada por múltiples hilos, o podría aparecer dos veces en la pila mientras el proceso se está ejecutando (es decir, la función podría llamarse a sí misma, directa o indirectamente). La función que toma devoluciones de llamadas como parámetros huele mucho.
Tenga en cuenta que la no reentrada es viral: una función que podría llamar a una posible función no reentrante no puede considerarse reentrada.
Tenga en cuenta también que los métodos de C ++ huelen porque tienen acceso a this
, por lo que debe estudiar el código para asegurarse de que no tengan una interacción divertida.
4.1. ¿Son todas las funciones recursivas reentrantes?
No.
En casos multiproceso, una función recursiva que accede a recursos compartidos podría ser invocada por varios hilos en el mismo momento, lo que daría como resultado datos incorrectos / dañados.
En casos singlethreaded, una función recursiva podría usar una función non-reentrant (como strtok
infame), o usar datos globales sin manejar el hecho de que los datos ya están en uso. Por lo tanto, su función es recursiva porque se llama directa o indirectamente, pero aún puede ser recursiva, insegura .
4.2. ¿Están todas las funciones de seguridad de subprocesos reentrantes?
En el ejemplo anterior, mostré cómo una función aparentemente insegura no se reentrató. Ok, hice trampa debido al parámetro Callback. Pero luego, hay varias formas de interbloquear un hilo haciendo que adquiera dos veces un bloqueo no recursivo.
4.3. ¿Están todas las funciones recursivas y seguras para subprocesos reentrantes?
Yo diría "sí" si por "recursivo" quiere decir "recursivo seguro".
Si puede garantizar que varios subprocesos pueden invocar simultáneamente una función y llamarse a sí mismo, directa o indirectamente, sin problemas, se reentrará.
El problema es evaluar esta garantía ... ^ _ ^
5. ¿Los términos como reentrada y seguridad de hilo son absolutos en absoluto, es decir, tienen definaciones fijas concretas?
Creo que tienen, pero luego, evaluar una función es seguro para subprocesos o reentrada puede ser difícil. Esta es la razón por la que utilicé el término " olor" anterior: puede encontrar que una función no es reentrante, pero podría ser difícil estar seguro de que una parte compleja del código es reentrante.
6. Un ejemplo
Supongamos que tiene un objeto, con un método que necesita usar recursos:
struct MyStruct
{
P * p ;
void foo()
{
if(this->p == NULL)
{
this->p = new P() ;
}
// Lots of code, some using this->p
if(this->p != NULL)
{
delete this->p ;
this->p = NULL ;
}
}
} ;
El primer problema es que si de alguna manera esta función se llama recursivamente (es decir, esta función se llama a sí misma, directa o indirectamente), el código probablemente fallará, porque this->p
se eliminará al final de la última llamada, y probablemente aún sea usado antes del final de la primera llamada.
Por lo tanto, este código no es recursivo, seguro .
Podríamos usar un contador de referencia para corregir esto:
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 = NULL ;
}
}
} ;
De esta forma, el código se convierte en recursivo-seguro ... Pero aún no se vuelve a ingresar debido a problemas de subprocesamiento múltiple: debemos estar seguros de que las modificaciones de c
y de p
se realizarán atómicamente, utilizando un mutex recursivo (no todos los mutex son recursivos) :
struct MyStruct
{
mutex m ; // recursive mutex
size_t c ;
P * p ;
void foo()
{
lock(m) ;
if(c == 0)
{
this->p = new P() ;
}
++c ;
unlock(m) ;
// Lots of code, some using this->p
lock(m) ;
--c ;
if(c == 0)
{
delete this->p ;
this->p = NULL ;
}
unlock(m) ;
}
} ;
Y, por supuesto, todo esto supone que la lots of code
sí mismo es reentrante, incluido el uso de p
.
Y el código anterior no es remotamente a prueba de exception-safe , pero esta es otra historia ... ^ _ ^
7. ¡Oye, el 99% de nuestro código no es reentrante!
Es bastante cierto para el código de spaghetti. Pero si particiona correctamente su código, evitará problemas de reentrada.
7.1. Asegúrese de que todas las funciones NO tengan estado.
Solo deben usar los parámetros, sus propias variables locales, otras funciones sin estado, y devolver copias de los datos si regresan.
7.2. Asegúrate de que tu objeto sea "recursivo seguro".
Un método de objeto tiene acceso a this
, por lo que comparte un estado con todos los métodos de la misma instancia del objeto.
Por lo tanto, asegúrese de que el objeto se pueda utilizar en un punto de la pila (es decir, llamando al método A) y luego, en otro punto (es decir, llamando al método B), sin corromper el objeto completo. Diseñe su objeto para asegurarse de que al salir de un método, el objeto sea estable y correcto (sin punteros colgantes, sin variables contradictorias, etc.).
7.3. Asegúrese de que todos sus objetos estén correctamente encapsulados.
Nadie más debería tener acceso a sus datos internos:
// 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 ;
}
Incluso devolver una referencia de referencia podría ser peligroso si el uso recupera la dirección de los datos, ya que otra parte del código podría modificarlo sin que se cuente el código que contiene la referencia de la referencia.
7.4. Asegúrese de que el usuario sepa que su objeto no es seguro para subprocesos
Por lo tanto, el usuario es responsable de usar mutexes para usar un objeto compartido entre hilos.
Los objetos del STL están diseñados para que no sean seguros para subprocesos (debido a problemas de rendimiento) y, por lo tanto, si un usuario desea compartir una std::string
entre dos subprocesos, el usuario debe proteger su acceso con primitivas de simultaneidad;
7.5. Asegúrese de que el código de seguridad de subprocesos sea recursivo seguro
Esto significa usar mutexes recursivos si cree que el mismo recurso puede ser usado dos veces por el mismo hilo.
"Con seguridad" se define exactamente como lo dicta el sentido común, significa "hacer las cosas correctamente sin interferir con otras cosas". Los seis puntos que cita claramente expresan los requisitos para lograr eso.
Las respuestas a tus 3 preguntas son 3 × "no".
¿Son todas las funciones recursivas reentrantes?
¡NO!
Dos invocaciones simultáneas de una función recursiva pueden confundirse fácilmente entre sí, si tienen acceso a los mismos datos globales / estáticos, por ejemplo.
¿Están todas las funciones de seguridad de subprocesos reentrantes?
¡NO!
Una función es segura para subprocesos si no funciona mal si se llama al mismo tiempo. Pero esto se puede lograr, por ejemplo, utilizando un mutex para bloquear la ejecución de la segunda invocación hasta que la primera finalice, de modo que solo funcione una invocación a la vez. Reentrada significa ejecutar simultáneamente sin interferir con otras invocaciones .
¿Están todas las funciones recursivas y seguras para subprocesos reentrantes?
¡NO!
Véase más arriba.
Ahora tengo que dar más detalles sobre mi comentario anterior. @paercebal la respuesta es incorrecta. En el código de ejemplo, ¿nadie notó que el mutex que, como se suponía, era un parámetro, en realidad no se pasó?
Discuto la conclusión, afirmo: para que una función sea segura en presencia de concurrencia, debe ser reentrante. Por lo tanto, concurrent-safe (generalmente escrito thread-safe) implica volver a entrar.
Ni el hilo seguro ni el reentrante tienen algo que decir sobre los argumentos: estamos hablando de la ejecución simultánea de la función, que aún puede ser insegura si se usan parámetros inapropiados.
Por ejemplo, memcpy () es seguro para subprocesos y reentrante (generalmente). Obviamente, no funcionará como se espera si se llama con punteros a los mismos objetivos de dos hilos diferentes. Ese es el punto de la definición de SGI, colocando la responsabilidad en el cliente para garantizar que el cliente sincronice los accesos a la misma estructura de datos.
Es importante comprender que, en general, no tiene sentido tener una operación segura para hilos que incluya los parámetros. Si ha realizado alguna programación de base de datos, lo entenderá. El concepto de lo que es "atómico" y podría estar protegido por un mutex u otra técnica es necesariamente un concepto de usuario: procesar una transacción en una base de datos puede requerir múltiples modificaciones sin interrupción. ¿Quién puede decir cuáles deben mantenerse sincronizados, pero el programador del cliente?
El punto es que la "corrupción" no tiene que ensuciar la memoria en su computadora con escrituras no serializadas: la corrupción aún puede ocurrir incluso si todas las operaciones individuales son serializadas. Se deduce que cuando se pregunta si una función es segura para subprocesos o reentrante, la pregunta significa para todos los argumentos adecuadamente separados: el uso de argumentos acoplados no constituye un contraejemplo.
Hay muchos sistemas de programación: Ocaml es uno, y creo que también Python, que tiene muchos códigos no reentrantes, pero que usa un bloqueo global para intercalar los procesos de subprocesos. Estos sistemas no son reentrantes y no son seguros para subprocesos o simultáneos, funcionan con seguridad simplemente porque impiden la concurrencia globalmente.
Un buen ejemplo es malloc. No es reentrante y no es seguro para subprocesos. Esto se debe a que tiene que acceder a un recurso global (el montón). Usar bloqueos no lo hace seguro: definitivamente no es reentrante. Si la interfaz para malloc tuviera un diseño adecuado, sería posible volverla a incorporar y segura para la ejecución de subprocesos:
malloc(heap*, size_t);
Ahora puede ser seguro porque transfiere la responsabilidad de serializar el acceso compartido a un solo montón para el cliente. En particular, no se requiere trabajo si hay objetos Heap separados. Si se utiliza un montón común, el cliente tiene que serializar el acceso. Usar un bloqueo dentro de la función no es suficiente: simplemente considere un malloc bloqueando un montón * y luego aparece una señal que llama a malloc en el mismo puntero: interbloqueo: la señal no puede continuar y el cliente tampoco puede hacerlo porque es interrumpido
En términos generales, las cerraduras no hacen que las roscas sean seguras ... en realidad destruyen la seguridad al tratar de administrar un recurso que es propiedad del cliente de manera inapropiada. Locking debe hacerlo el fabricante del objeto, es el único código que sabe cuántos objetos se crean y cómo se usarán.
El "hilo común" (¡juego de palabras intencionado !?) entre los puntos enumerados es que la función no debe hacer nada que pueda afectar el comportamiento de cualquier llamada recursiva o concurrente a la misma función.
Entonces, por ejemplo, los datos estáticos son un problema porque pertenecen a todos los hilos; si una llamada modifica una variable estática, todos los hilos utilizan los datos modificados, lo que afecta su comportamiento. El código de auto modificación (aunque rara vez se encuentra, y en algunos casos se previene) sería un problema, porque aunque hay múltiples hilos, solo hay una copia del código; el código también es información estática esencial.
Esencialmente para ser reentrante, cada hilo debe ser capaz de usar la función como si fuera el único usuario, y ese no es el caso si un hilo puede afectar el comportamiento de otro de una manera no determinista. En primer lugar, esto implica que cada hilo tenga datos separados o constantes sobre los que funciona la función.
Todo lo dicho, el punto (1) no es necesariamente verdadero; por ejemplo, puede utilizar legítimamente y por diseño una variable estática para retener un recuento de recursión para protegerse contra una recursión excesiva o para perfilar un algoritmo.
Una función de hilo seguro no necesita ser reentrada; puede lograr la seguridad del hilo evitando específicamente la reentrada con un bloqueo, y el punto (6) dice que dicha función no se reentrante. Con respecto al punto (6), una función que llama a una función de seguridad de subprocesos que bloquea no es segura para su uso en recursión (se bloqueará), y por lo tanto no se dice que es reentrante, aunque puede ser segura para concurrencia, y aún sería reentrante en el sentido de que múltiples hilos pueden tener sus contadores de programa en dicha función simultáneamente (solo que no con la región bloqueada). Puede ser que esto ayude a distinguir la seguridad de la hebra de la referencia (¡o quizás aumente tu confusión!).
El hilo común:
¿Está bien definido el comportamiento si se invoca la rutina mientras se interrumpe?
Si tienes una función como esta:
int add( int a , int b ) {
return a + b;
}
Entonces no depende de ningún estado externo. El comportamiento está bien definido.
Si tienes una función como esta:
int add_to_global( int a ) {
return gValue += a;
}
El resultado no está bien definido en múltiples hilos. La información podría perderse si el tiempo fue simplemente incorrecto.
La forma más simple de una función reentrante es algo que opera exclusivamente sobre los argumentos pasados y los valores constantes. Cualquier otra cosa requiere un manejo especial o, a menudo, no es reentrante. Y, por supuesto, los argumentos no deben hacer referencia a variables globales mutables.
Las respuestas a sus preguntas "También" son "No", "No" y "No". El hecho de que una función sea recursiva y / o segura para hilos no la convierte en reentrante.
Cada uno de estos tipos de funciones puede fallar en todos los puntos que cita. (Aunque no estoy 100% seguro del punto 5).
Los términos "Thread-safe" y "re-entrant" significan solo y exactamente lo que dicen sus definiciones. "Seguro" en este contexto significa solo lo que dice la definición que cita a continuación.
"Seguro" aquí ciertamente no significa seguro en el sentido más amplio de que llamar a una función determinada en un contexto dado no manguera totalmente su aplicación. En general, una función puede producir de manera fiable un efecto deseado en su aplicación de subprocesos múltiples, pero no puede calificarse como reentrante o como subproceso según las definiciones. De manera opuesta, puede llamar a las funciones de reentrada de manera que producirán una variedad de efectos no deseados, inesperados y / o impredecibles en su aplicación de subprocesos múltiples.
La función recursiva puede ser cualquier cosa y la Reentrada tiene una definición más sólida que la seguridad de subprocesos, por lo que las respuestas a las preguntas numeradas son todas no.
Al leer la definición de reentrante, uno podría resumirlo como una función que no modificará nada más allá de lo que usted llama para modificar. Pero no deberías confiar solo en el resumen.
La programación con múltiples subprocesos es extremadamente difícil en el caso general. Saber qué parte del reentrada del código es solo una parte de este desafío. La seguridad del hilo no es aditiva. En lugar de intentar unir las funciones de reentrantes, es mejor utilizar un patrón de diseño global thread-safe y usar este patrón para guiar el uso de cada hilo y los recursos compartidos en el programa.