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:
main
llamará foo
.
foo
adquirirá la cerradura.
foo
llamará bar
, que llamaráfoo
.
- el segundo
foo
intentará adquirir el bloqueo, fallará y esperará a que se libere.
- Punto muerto.
- 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:
- La función tiene un estado (es decir, acceder 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 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 c
y p
se 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 code
es 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::string
entre 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.