¿Qué es exactamente una función de reentrada?


Most de las times , la definición de reentrada se cita de Wikipedia :

Un programa de computadora o rutina es descrito como reentrante si puede ser de forma segura llamado de nuevo antes de su se ha completado la invocación anterior (es decir, se puede ejecutar con seguridad simultáneamente). Para ser reentrante, un programa o rutina de computadora:

  1. No debe contener estática (o global) no constante datos.
  2. no Debe devolver la dirección a estática (o global) no constante datos.
  3. Debe trabajar solo en los datos proporcionados a él por la persona que llama.
  4. No debe confiar en los bloqueos para singleton recurso.
  5. No debe modificar su propio código (a menos que ejecución en su propio hilo único almacenamiento)
  6. No debe llamar al equipo no reentrante programas o rutinas.

¿Cómo es con seguridad definido?

Si un programa puede ser ejecutado de forma segura al mismo tiempo, ¿siempre significa que es reentrante?

¿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,

  1. ¿Son todas las funciones recursivas reentrantes?
  2. ¿Se reentran todas las funciones seguras para subprocesos?
  3. Son todas funciones recursivas y seguras para subprocesos ¿reingresar?

Al escribir esta pregunta, una cosa viene a la mente: ¿Son los términos como reentrada y seguridad del hilo absolutos en absoluto, es decir, tienen definiciones concretas fijas? Porque, si no lo son, esta pregunta no tiene mucho sentido.

Author: Community, 2010-05-10

7 answers

1. ¿Cómo se define de forma segura ?

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

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

No.

Por ejemplo, vamos a tener una función de C++ que toma tanto un bloqueo, y una devolución de llamada como un 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 bien podría necesitar bloquear el mismo mutex:

void bar()
{
    foo(nullptr);
}

At a primera vista, todo parece estar bien wait 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. main llamará a foo.
  2. foo adquirirá la cerradura.
  3. foo llamará a bar, que llamará a foo.
  4. el 2do foo intentará adquirir el bloqueo, fallará y esperará a que sea liberado.
  5. Punto muerto.
  6. Oops {

Ok, hice trampa, usando la cosa 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 debe oler, entonces See de manejar que...)

Entonces, estudiando tu código, uno de esos puntos debería alertarte: {[28]]}

  1. La función tiene un estado (es decir, acceder a una variable global, o incluso a una variable miembro de clase)
  2. Esta función puede ser llamada por múltiples subprocesos, 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). Función que toma callbacks 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 reentrante.

Tenga en cuenta, también, que los métodos C++ huelen porque tienen acceso a this, por lo que debe estudiar el código para asegurarse de que no tienen ninguna 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 llamada por varios subprocesos al mismo tiempo momento, lo que resulta en datos malos/dañados.

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

4.2. ¿Vuelven a entrar todas las funciones seguras para hilos?

En el ejemplo anterior, mostré cómo un aparentemente threadsafe la función no era reentrante. Ok Hice trampa debido al parámetro callback. Pero entonces, hay varias maneras de bloquear un hilo haciendo que adquiera dos veces un bloqueo no recursivo.

4.3. ¿Vuelven a entrar todas las funciones recursivas y seguras para el hilo?

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

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. ¿Son los términos como reentrada y seguridad del hilo absolutos en absoluto, es decir, tienen definiciones concretas fijas?

Creo que lo han hecho, pero entonces, evaluar una función es thread-safe o reentrante puede ser difícil. Es por eso que usé el término smell arriba: Puedes encontrar que una función no es reentrante, pero podría ser difícil estar seguro de que una pieza compleja de código es reentrante

6. Un ejemplo

Digamos que tienes un objeto, con un método que necesita usar recursos: {[28]]}

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 recursivamente (es decir, esta función se llama a sí misma, directa o indirectamente), el código probablemente se bloqueará, porque this->p se eliminará al final de la última llamada, y probablemente todavía se utilizará 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 = nullptr;
        }
    }
};

De esta manera, el código se vuelve recursivo-seguro But Pero todavía no es reentrante debido a problemas de multihilo: Debemos estar seguros de que las modificaciones de c y de p se harán atómicamente, utilizando un recursivo mutex (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 asume que el lots of code es en sí mismo reentrante, incluyendo el uso de p.

Y el código anterior no es ni remotamente exception-safe , pero esta es otra historia ^^_ ^

7. Hey 99% de nuestro código no es reentrante!

Es bastante cierto para spaghetti code. Pero si particiona correctamente su código, evitará problemas de reentrada.

7.1. Asegúrese de que todas las funciones no tienen 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úrese de que su el objeto es"recursivo-seguro"

Un método 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 puede 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 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 de miembro contradictorias, sucesivamente.).

7.3. Asegúrese de que todos sus objetos estén correctamente encapsulados

Nadie más debe 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 const podría ser peligroso si el uso recupera la dirección de los datos, ya que alguna otra parte del código podría modificarla sin que se le diga al código que contiene la referencia const.

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 utilizar mutexes para usar un objeto compartido entre subprocesos.

Los objetos de la 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 un std::string entre dos subprocesos, el usuario debe proteger su acceso con primitivas de concurrencia;

7.5. Asegúrese de que el código thread-safe es recursivo-seguro

Esto significa usar mutexes recursivos si crees que el mismo recurso puede ser usado dos veces por el mismo hilo.

 167
Author: paercebal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-24 18:42:06

"Con seguridad" se define exactamente como lo dicta el sentido común - significa "hacer sus cosas correctamente 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".


¿Son todas las funciones recursivas reentrantes?

¡NO!

Dos invocaciones simultáneas de una función recursiva pueden fácilmente joderse entre sí, si acceden a la misma datos globales / estáticos, por ejemplo.


¿Vuelven a entrar todas las funciones seguras para hilos?

¡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 termine la primera, por lo que solo una invocación funciona a la vez. Reentrada significa ejecutar simultáneamente sin interferir con otras invocaciones.


Son ¿todas las funciones recursivas y seguras para hilos vuelven a entrar?

¡NO!

Véase más arriba.

 20
Author: slacker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-09 20:39:05

El hilo común:

¿Está bien definido el comportamiento si se llama a 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 subprocesos. La información podría perderse si el momento era simplemente incorrecto.

La forma más simple de una función reentrante es algo que opera exclusivamente en los argumentos pasados y 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.

 10
Author: drawnonward,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-09 20:50:22

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

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

Ni thread safe ni re-entrant tienen nada que decir sobre los argumentos: estamos hablando de la ejecución concurrente de la función, que todavía 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 destinos desde dos hilos diferentes. Ese es el punto de la definición SGI, colocando la responsabilidad en el cliente para garantizar que los accesos a la misma estructura de datos estén sincronizados por el cliente.

Es importante entender que en general es absurdo que el funcionamiento seguro del hilo incluya los parámetros. Si usted ha hecho cualquier programación de la base de datos usted entenderá. El concepto de lo que es "atómico" y podría estar protegido por un mutex o alguna otra técnica es necesariamente un concepto de usuario: el procesamiento de una transacción en una base de datos puede requerir múltiples modificaciones no interrumpidas. ¿Quién puede decir cuáles deben mantenerse sincronizados sino el programador cliente?

El punto es que la "corrupción" no tiene que ser estropear la memoria en su computadora con escrituras no serializadas: la corrupción todavía 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 apropiadamente separados: el uso de argumentos acoplados no constituye un contraejemplo.

Hay muchos sistemas de programación por ahí: Ocaml es uno, y creo que Python también, que tienen un montón de código no reentrante en ellos, pero que utiliza un bloqueo global para intercalar accesos de hilo. Estos sistemas no son reentrantes y no son seguros para subprocesos ni seguros para concurrentes, operan de forma segura simplemente porque evitan la concurrencia a nivel global.

Un buen ejemplo es malloc. No es reentrante y no hilo seguro. Esto se debe a que tiene que acceder a un recurso global (el montón). El uso de cerraduras no lo hace seguro: definitivamente no es reentrante. Si la interfaz a malloc se hubiera diseñado correctamente sería posible hacerla reentrante y thread-safe:

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 ningún trabajo si hay objetos de montón separados. Si se utiliza un montón común, el cliente tiene que serializar el acceso. Usando un lock 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: deadlock: la señal no puede proceda, y el cliente tampoco puede porque se interrumpe.

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

 7
Author: Yttrill,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-12-10 05:36:26

El "hilo común" (juego de palabras!?) 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.

Por ejemplo, los datos estáticos son un problema porque son propiedad de todos los subprocesos; si una llamada modifica una variable estática, todos los subprocesos utilizan los datos modificados, lo que afecta su comportamiento. Código de auto modificación (aunque rara vez se encuentra, y en algunos casos prevenido) sería un problema, porque aunque hay varios subprocesos, solo hay una copia del código; el código también es esencial para los datos estáticos.

Esencialmente para ser reentrante, cada hilo debe ser capaz de utilizar 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 hilo tenga datos separados o constantes en los que trabaja la función.

Dicho esto, el punto (1) no es necesariamente verdadero; por ejemplo, puede usar legítimamente y por diseño una variable estática para retener un recuento de recursiones para protegerse contra una recursión excesiva o para perfilar un algoritmo.

Una función segura para subprocesos no necesita ser reentrada; puede lograr la seguridad del subproceso evitando específicamente la reentrada con un bloqueo, y el punto (6) dice que tal función no es reentrada. En relación con el punto 6, una función que llama a una función segura para hilos que se bloquea no es segura para su uso en recursión (lo hará dead-lock), y por lo tanto no se dice que sea reentrante, aunque no obstante puede ser seguro para la concurrencia, y todavía sería reentrante en el sentido de que múltiples hilos pueden tener sus contadores de programa en tal función simultáneamente (solo que no con la región bloqueada). Puede ser que esto ayude a distinguir la seguridad del hilo de la reencarnación (¡o tal vez aumente su confusión!).

 3
Author: Clifford,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-09 20:47:46

Las respuestas a tus preguntas "También" son "No", "No" y "No". El hecho de que una función sea recursiva y/o segura para subprocesos no la convierte en reentrante.

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

 1
Author: ChrisF,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-09 20:19:40

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 dada en un contexto dado no va a transmitir totalmente su aplicación. En conjunto, una función puede producir de manera confiable el efecto deseado en su aplicación multihilo, pero no califica como reentrante o seguro para roscas según las definiciones. Al contrario, puede llamar a las funciones de reentrada de maneras que producirán una variedad de efectos no deseados, inesperados y/o impredecibles en su aplicación multihilo.

La función recursiva puede ser cualquier cosa y el reentrante tiene una definición más fuerte que thread-safe, por lo que las respuestas a sus preguntas numeradas son todas no.

Leyendo la definición de reentrante, uno podría resumirla como una función que no modificará nada más allá de lo que tú llamas modificar. Pero no debes confiar solo en el resumen.

La programación multihilo es simplemente extremadamente difícil en el caso general. Saber qué parte del código reentrante es solo una parte de este desafío. La seguridad del hilo no es aditiva. En lugar de intentar juntar las funciones de reentrada, es mejor usar un thread-safe general diseñe el patrón y use este patrón para guiar su uso de cada hilo y compartido recursos en su programa.

 1
Author: Joe Soul-bringer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-10 01:19:13