¿Qué es exactamente una función reentrante?

198

La mayor parte de las veces , 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 forma segura antes de que se haya completado su invocación anterior (es decir, se puede ejecutar de manera segura al mismo tiempo). Para ser reentrante, un programa de computadora o rutina:

  1. No debe contener datos estáticos (o globales) no constantes.
  2. No debe devolver la dirección a datos estáticos (o globales) no constantes.
  3. Debe funcionar solo con los datos que le proporcionó la persona que llama.
  4. No debe confiar en bloqueos para recursos únicos.
  5. No debe modificar su propio código (a menos que se ejecute en su propio almacenamiento único de subprocesos)
  6. No debe llamar a programas de computadora o rutinas no reentrantes.

Cómo es con seguridad define con ?

Si un programa se puede ejecutar de manera segura al mismo tiempo , ¿significa siempre que es reentrante?

¿Cuál es exactamente el hilo conductor entre los seis puntos mencionados que debo tener en cuenta al verificar mi código para las capacidades reentrantes?

También,

  1. ¿Todas las funciones recursivas son reentrantes?
  2. ¿Todas las funciones seguras para subprocesos son reentrantes?
  3. ¿Todas las funciones recursivas y seguras para hilos son reentrantes?

Al escribir esta pregunta, me viene a la mente una cosa: ¿son absolutos los términos reentrada y seguridad del hilo, es decir, tienen definiciones concretas fijas? Porque, si no lo son, esta pregunta no es muy significativa.

Lazer
fuente
66
En realidad, no estoy de acuerdo con el n. ° 2 en la primera lista. Puede devolver una dirección a lo que desee desde una función reentrante; la limitación está en lo que hace con esa dirección en el código de llamada.
2
@Neil ¿Pero como el escritor de la función reentrante no puede controlar lo que la persona que llama seguramente no debe devolver una dirección a datos no constantes estáticos (o globales) para que sea verdaderamente reentrante?
Robben_Ford_Fan_boy
2
@drelihan No es responsabilidad del escritor de CUALQUIER función (reentrante o no) controlar lo que hace una persona que llama con un valor devuelto. Sin duda, deberían decir lo que la persona que llama PUEDE hacer con él, pero si la persona que llama elige hacer otra cosa, mala suerte para la persona que llama.
"seguro para subprocesos" no tiene sentido a menos que también especifique qué están haciendo los subprocesos y cuál es el efecto esperado de sus acciones. Pero tal vez esa debería ser una pregunta separada.
Considero seguro que el comportamiento está bien definido y es determinista, independientemente de la programación.
AturSams

Respuestas:

191

1. ¿Cómo es con seguridad define con ?

Semánticamente En este caso, este no es un término definido. Simplemente significa "Puedes hacer eso, sin riesgo".

2. Si un programa puede ejecutarse de manera segura al mismo tiempo, ¿significa siempre que es reentrante?

No.

Por ejemplo, tengamos una función de C ++ que tome tanto un bloqueo como una devolución de llamada como parámetro:

#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();
}

Otra función podría necesitar bloquear el mismo mutex:

void bar()
{
    foo(nullptr);
}

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:

  1. mainllamará foo.
  2. foo adquirirá la cerradura.
  3. foollamará bar, que llamaráfoo .
  4. el segundo foo intentará adquirir el bloqueo, fallará y esperará a que se libere.
  5. Punto muerto.
  6. Ups ...

Ok, hice trampa, usando la 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 conductor entre los seis puntos mencionados que debo tener en cuenta al verificar mi código para las capacidades reentrantes?

Puede detectar un problema si su función tiene / le da acceso a un recurso persistente modificable, o tiene / le da acceso a una función que huele .

( Ok, el 99% de nuestro código debe oler, entonces ... Ver la última sección para manejar eso ... )

Entonces, al estudiar su código, uno de esos puntos debería alertarlo:

  1. La función tiene un estado (es decir, acceder a una variable global, o incluso a una variable miembro de la clase)
  2. Esta función puede ser llamada por múltiples hilos, o podría aparecer dos veces en la pila mientras se ejecuta el proceso (es decir, la función podría llamarse a sí misma, directa o indirectamente). La función que toma devoluciones de llamada como parámetros huele mucho.

Tenga en cuenta que la no reentrada es viral: una función que podría llamar una posible función no reentrante no puede considerarse reentrante.

Tenga en cuenta también que los métodos C ++ huelen porque tienen acceso a ellos this, por lo que debe estudiar el código para asegurarse de que no tengan una interacción divertida.

4.1. ¿Todas las funciones recursivas son reentrantes?

No.

En casos de subprocesos múltiples, varios subprocesos pueden invocar una función recursiva que accede a un recurso compartido al mismo tiempo, lo que da como resultado datos incorrectos / corruptos.

En casos de subproceso único, una función recursiva podría usar una función no reentrante (como la infame strtok), o usar datos globales sin manejar el hecho de que los datos ya están en uso. Entonces su función es recursiva porque se llama a sí misma directa o indirectamente, pero aún puede ser recursiva-insegura .

4.2. ¿Todas las funciones seguras para subprocesos son reentrantes?

En el ejemplo anterior, mostré cómo una función aparentemente segura no era reentrante. OK, hice trampa por el parámetro de devolución de llamada. Pero entonces, hay varias formas de bloquear un hilo haciendo que adquiera dos veces un bloqueo no recursivo.

4.3. ¿Todas las funciones recursivas y seguras para hilos son reentrantes?

Yo diría "sí" si por "recursivo" quiere decir "seguro recursivo".

Si puede garantizar que una función puede ser llamada simultáneamente por múltiples hilos, y puede llamarse a sí misma, directa o indirectamente, sin problemas, entonces es reentrante.

El problema es evaluar esta garantía ... ^ _ ^

5. ¿Los términos como reentrada y seguridad de roscas son absolutos, es decir, tienen definiciones concretas fijas?

Creo que sí, pero luego, evaluar una función es segura para los hilos o reentrante puede ser difícil. Por eso usé el término olor arriba: puede encontrar que una función no es reentrante, pero podría ser difícil asegurarse de que un código complejo sea reentrante

6. Un ejemplo

Digamos que tiene un objeto, con un método que necesita usar un recurso:

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

El primer problema es que si de alguna manera esta función se llama de forma recursiva (es decir, esta función se llama a sí misma, directa o indirectamente), el código probablemente se bloqueará, porque this->p que se eliminará al final de la última llamada y probablemente todavía se usará antes del final de la primera convocatoria.

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

De esta manera, el código se convierte en recursivo seguro ... Pero aún no es reentrante debido a problemas de subprocesamiento múltiple: debemos estar seguros de que las modificaciones de cy pse realizarán atómicamente, utilizando un mutex recursivo (no todos los mutexes son recursivos):

#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();
    }
};

Y, por supuesto, todo esto supone que lots of codees reentrante, incluido el uso dep .

Y el código anterior no es ni remotamente seguro para excepciones , pero esta es otra historia ... ^ _ ^

7. ¡Hola, el 99% de nuestro código no es reentrante!

Es bastante cierto para el código de espagueti. 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 vuelven.

7.2. Asegúrese de que su objeto sea "recursivo seguro"

Un método de objeto tiene acceso 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 pueda usarse en un punto de la pila (es decir, llamar al método A) y luego, en otro punto (es decir, llamar al método B), sin corromper todo el objeto. 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 miembro 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 constante podría ser peligroso si el usuario recupera la dirección de los datos, ya que otra parte del código podría modificarla sin que se diga el código que contiene la referencia constante.

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 no ser seguros para subprocesos (debido a problemas de rendimiento) y, por lo tanto, si un usuario desea compartir uno std::stringentre dos subprocesos, el usuario debe proteger su acceso con primitivas de concurrencia;

7.5. Asegúrese de que su código seguro para subprocesos sea seguro para uso recursivo

Esto significa usar mutex recursivos si cree que el mismo hilo puede usar dos veces el mismo recurso.

paercebal
fuente
1
Para discutir un poco, en realidad creo que en este caso se define "seguridad", lo que significa que la función actuará solo en las variables proporcionadas, es decir, es la abreviatura de la cita de definición debajo de ella. Y el punto es que esto podría no implicar otras ideas de seguridad.
Joe Soul-bringer
¿Te perdiste pasar el mutex en el primer ejemplo?
desvío
@paercebal: tu ejemplo está mal. En realidad, no necesita molestarse con la devolución de llamada, una recursión simple tendría el mismo problema si hay una, sin embargo, el único problema es que olvidó decir exactamente dónde está asignado el bloqueo.
Hasta el
3
@Yttrill: Supongo que estás hablando del primer ejemplo. Usé la "devolución de llamada" porque, en esencia, una devolución de llamada huele mal. Por supuesto, una función recursiva tendría el mismo problema, pero por lo general, uno puede analizar fácilmente una función y su naturaleza recursiva, y así detectar si es reentrante o si está bien para la recursividad. La devolución de llamada, por otro lado, significa que el autor de la función que llama a la devolución de llamada no tiene información sobre lo que está haciendo la devolución de llamada, por lo que este autor puede tener dificultades para asegurarse de que su función sea reentrante. Esta es la dificultad que quería mostrar.
paercebal
1
@Gab 是 好人: he corregido el primer ejemplo. ¡Gracias! Un manejador de señales vendría con sus propios problemas, distintos de la reentrada, ya que generalmente, cuando se eleva una señal, no se puede hacer nada más que cambiar una variable global específicamente declarada.
paercebal
21

"Con seguridad" se define exactamente como lo dicta el sentido común: significa "hacer lo correcto sin interferir con otras cosas". Los seis puntos que usted cita expresan claramente los requisitos para lograrlo.

Las respuestas a sus 3 preguntas son 3 × "no".


¿Todas las funciones recursivas son reentrantes?

¡NO!

Dos invocaciones simultáneas de una función recursiva pueden arruinarse fácilmente, por ejemplo, si acceden a los mismos datos globales / estáticos.


¿Todas las funciones seguras para subprocesos son reentrantes?

¡NO!

Una función es segura para subprocesos si no funciona mal si se llama simultáneamente. Pero esto se puede lograr, por ejemplo, mediante el uso de un mutex para bloquear la ejecución de la segunda invocación hasta que finalice la primera, por lo que solo una invocación funciona a la vez. Reentrada significa ejecutar simultáneamente sin interferir con otras invocaciones .


¿Todas las funciones recursivas y seguras para hilos son reentrantes?

¡NO!

Véase más arriba.

vago
fuente
10

El hilo conductor:

¿Está bien definido el comportamiento si se llama a la rutina mientras se interrumpe?

Si tiene 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 tiene 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 momento era 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 globales mutables.

dibujado hacia adelante
fuente
7

Ahora tengo que elaborar mi comentario anterior. La respuesta @paercebal es incorrecta. En el código de ejemplo, ¿nadie se dio cuenta de que el mutex que, como se suponía que era un parámetro, no se pasó realmente?

Discuto la conclusión, afirmo: para que una función sea segura en presencia de concurrencia, debe ser reentrada. Por lo tanto, concurrent-safe (generalmente escrito thread-safe) implica reentrante.

Ni el subproceso seguro ni el reentrante tienen nada que decir sobre los argumentos: estamos hablando de la ejecución concurrente de la función, que aún puede ser insegura si se utilizan 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 desde dos hilos diferentes. Ese es el punto de la definición de SGI, que corresponde al cliente para garantizar que el cliente sincronice los accesos a la misma estructura de datos.

Es importante comprender que, en general, no tiene sentido que la operación segura para subprocesos incluya los parámetros. Si ha realizado alguna programación de base de datos, lo comprenderá. El concepto de lo que es "atómico" y que 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 sino el programador del cliente?

El punto es que la "corrupción" no tiene que estar desordenando la memoria de su computadora con escrituras no serializadas: la corrupción aún puede ocurrir incluso si todas las operaciones individuales se serializan. De esto se deduce que cuando se pregunta si una función es segura para subprocesos o reentrante, la pregunta significa para todos los argumentos separados de manera apropiada: el uso de argumentos acoplados no constituye un contraejemplo.

Existen muchos sistemas de programación: Ocaml es uno, y creo que Python también, que tiene muchos códigos no reentrantes, pero que usa un bloqueo global para intercalar los accesos de subprocesos. Estos sistemas no son reentrantes y no son seguros para subprocesos o concurrentes, operan de manera segura simplemente porque evitan la concurrencia global.

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). El uso de bloqueos no lo hace seguro: definitivamente no es reentrante. Si la interfaz de malloc se hubiera diseñado correctamente, sería posible volverla a ingresar y proteger contra subprocesos:

malloc(heap*, size_t);

Ahora puede ser seguro porque transfiere la responsabilidad de serializar el acceso compartido a un único montón al cliente. En particular, no se requiere trabajo si hay objetos de montón separados. Si se usa un montón común, el cliente debe serializar el acceso. Usar un bloqueo dentro de la función no es suficiente: solo considere un malloc bloqueando un montón * y luego aparece una señal y llama a malloc en el mismo puntero: punto muerto: la señal no puede continuar, y el cliente tampoco puede porque es interrumpido

En términos generales, las cerraduras no hacen que las cosas sean seguras para los subprocesos ... en realidad destruyen la seguridad al tratar inapropiadamente de administrar un recurso que es propiedad del cliente. El bloqueo debe ser realizado por el fabricante del objeto, ese es el único código que sabe cuántos objetos se crean y cómo se utilizarán.

Yttrill
fuente
"Por lo tanto, concurrent-safe (generalmente escrito thread-safe) implica reentrante". Esto contradice el ejemplo de Wikipedia "A prueba de hilos pero no reentrante" .
Maggyero
3

El "hilo común" (juego de palabras intencionado?) Entre los puntos enumerados es que la función no debe hacer nada que afecte el comportamiento de cualquier llamada recursiva o concurrente a la misma función.

Entonces, por ejemplo, los datos estáticos son un problema porque son propiedad de 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 modificación automática (aunque rara vez se encuentra, y en algunos casos se evita) sería un problema, porque aunque hay varios subprocesos, 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. Principalmente, esto implica que cada subproceso tenga datos separados o constantes en los que trabaja la función.

Todo lo dicho, el punto (1) no es necesariamente cierto; por ejemplo, podría legítimamente y, por diseño, usar una variable estática para retener un recuento de recursividad para evitar una recursividad excesiva o para perfilar un algoritmo.

Una función segura para subprocesos no necesita ser reentrante; puede lograr la seguridad del hilo evitando específicamente la reentrada con un bloqueo, y el punto (6) dice que dicha función no es reentrante. Con respecto al punto (6), una función que llama a una función segura para subprocesos que se bloquea no es segura para su uso en recursión (se bloqueará) y, por lo tanto, no se dice que sea reentrante, aunque puede ser segura para la concurrencia, y seguiría siendo reentrante en el sentido de que varios subprocesos 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 subprocesos de la renta (¡o tal vez aumenta su confusión!).

Clifford
fuente
1

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 subprocesos no la hace reentrada.

Cada uno de estos tipos de funciones puede fallar en todos los puntos que cita. (Aunque no estoy 100% seguro del punto 5).

ChrisF
fuente
1

Los términos "seguro para subprocesos" y "reentrante" solo significan y exactamente lo que dicen sus definiciones. "Seguro" en este contexto significa solo lo que dice la definición que cita debajo.

"Seguro" aquí ciertamente no significa seguro en el sentido más amplio de que llamar a una función dada en un contexto dado no afectará totalmente su aplicación. En conjunto, una función puede producir de manera confiable un efecto deseado en su aplicación de subprocesos múltiples, pero no califica como reentrante o seguro para subprocesos según las definiciones. De manera opuesta, puede llamar a funciones reentrantes de manera que produzca una variedad de efectos no deseados, inesperados y / o impredecibles en su aplicación multiproceso.

La función recursiva puede ser cualquier cosa y Re-entrante tiene una definición más sólida que segura para subprocesos, por lo que las respuestas a sus 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 modificar. Pero no debe confiar solo en el resumen.

La programación multiproceso es extremadamente difícil en el caso general. Saber qué parte del código entrante es solo una parte de este desafío. La seguridad del hilo no es aditiva. En lugar de tratar de juntar las funciones reentrantes, es mejor usar un patrón de diseño seguro para subprocesos y usar este patrón para guiar su uso de cada subproceso y recursos compartidos en su programa.

Joe portador de almas
fuente