¿Es seguro tenedor desde dentro de un hilo?

Permítanme explicar: ya he estado desarrollando una aplicación en Linux que bifurca y ejecuta un binario externo y espera a que termine. Los resultados se comunican mediante archivos shm que son exclusivos del proceso fork +. El código completo está encapsulado dentro de una clase.

Ahora estoy considerando enhebrar el proceso para acelerar las cosas. Al tener muchas instancias diferentes de funciones de clase, bifurque y ejecute el binario simultáneamente (con diferentes parámetros) y comunique los resultados con sus propios archivos shm únicos.

¿Este hilo es seguro? Si bifurco dentro de un hilo, además de estar a salvo, ¿hay algo que tenga que vigilar? ¡Cualquier consejo o ayuda es muy apreciada!

fork , incluso con hilos, es seguro. Una vez que se bifurca, los hilos son independientes por proceso. (Es decir, el enhebrado es ortogonal a la bifurcación). Sin embargo, si los hilos en procesos diferentes usan la misma memoria compartida para comunicarse, debe diseñar un mecanismo de sincronización.

El problema es que fork () solo copia el hilo que realiza la llamada y cualquier mutex retenido en los hilos secundarios se bloqueará para siempre en el elemento secundario bifurcado. La solución pthread fue pthread_atfork() . La idea era que usted puede registrar 3 manejadores: uno prefork, un manejador principal y un manejador secundario. Cuando fork() ocurre prefork se llama antes de la horquilla y se espera que obtenga todos los mutexes de la aplicación. Tanto el padre como el hijo deben liberar todos los mutex en los procesos primarios y secundarios, respectivamente.

¡Este no es el final de la historia! Las bibliotecas llaman a pthread_atfork para registrar manejadores para mutexes específicos de la biblioteca, por ejemplo, Libc hace esto. Esto es algo bueno: la aplicación no puede saber acerca de los mutexes en bibliotecas de terceros, por lo que cada biblioteca debe llamar a pthread_atfork para asegurarse de que sus propios mutexes se limpien en el caso de un fork() .

El problema es que el orden en que se llaman los manejadores pthread_atfork para bibliotecas no relacionadas no está definido (depende del orden en que el progtwig cargue las bibliotecas). Esto significa que técnicamente puede producirse un punto muerto dentro de un controlador prefork debido a una condición de carrera.

Por ejemplo, considere esta secuencia:

  1. Thread T1 llama fork()
  2. prefork handlers para libc obtenido en T1
  3. A continuación, en Thread T2, una biblioteca de terceros A adquiere su propio mutex AM y luego realiza una llamada libc que requiere un mutex. Esto bloquea, porque los mutex de libc son retenidos por T1.
  4. El subproceso T1 ejecuta el controlador prefork para la biblioteca A, que bloquea la espera para obtener AM, que está retenido por T2.

Está su punto muerto y no está relacionado con su propio mutex o código.

Esto realmente sucedió en un proyecto en el que trabajé una vez. El consejo que había encontrado en ese momento era elegir tenedor o hilos, pero no ambos. Pero para algunas aplicaciones eso probablemente no sea práctico.

Es seguro tenedor en un progtwig multiproceso, siempre y cuando tengas mucho cuidado con el código entre fork y exec. Solo puede hacer llamadas al sistema reentradas (también conocidas como asincrónicamente seguras) en ese lapso. En teoría, no está permitido realizar malloc o liberarlo allí, aunque en la práctica el asignador de Linux predeterminado es seguro y las bibliotecas de Linux llegaron a confiar en él. El resultado final es que debe usar el asignador predeterminado.

Si bien puede utilizar el soporte NPTL pthreads(7) Linux para su progtwig, los subprocesos son un ajuste incómodo en los sistemas Unix, como ha descubierto con su pregunta del fork(2) .

Dado que la fork(2) es una operación muy económica en los sistemas modernos, es mejor que simplemente fork(2) su proceso cuando tenga que realizar más maniobras. Depende de la cantidad de datos que pretenda mover hacia adelante y hacia atrás, la filosofía de compartir fork procesos fork es buena para reducir errores de datos compartidos, pero significa que necesita crear pipes para mover datos entre procesos o usar memoria compartida ( shmget(2) o shm_open(3) ).

Pero si opta por utilizar el enhebrado, puede fork(2) un nuevo proceso, con las siguientes sugerencias de la página de manual de fork(2) :

  * The child process is created with a single thread — the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause. 

De vuelta en el origen del tiempo, llamamos a los hilos “procesos livianos” porque si bien se parecen mucho a los procesos, no son idénticos. La mayor distinción es que los hilos, por definición, viven en el mismo espacio de direcciones de un proceso. Esto tiene sus ventajas: cambiar de subproceso a subproceso es rápido, ellos comparten la memoria intrínsecamente para que las comunicaciones entre subprocesos sean rápidas, y la creación y eliminación de subprocesos es rápida.

La distinción aquí es con “procesos pesados”, que son espacios completos de direcciones. Un nuevo proceso de peso pesado es creado por fork (2) . A medida que la memoria virtual entró en el mundo de UNIX, se aumentó con vfork (2) y algunos otros.

Un tenedor (2) copia todo el espacio de direcciones del proceso, incluidos todos los registros, y pone ese proceso bajo el control del progtwigdor del sistema operativo; la próxima vez que aparece el progtwigdor, el contador de instrucciones retoma la siguiente instrucción: el proceso hijo bifurcado es una clonación del padre. (Si desea ejecutar otro progtwig, por ejemplo, porque está escribiendo un intérprete de comandos, sigue el tenedor con una llamada a exec (2) , que carga ese nuevo espacio de direcciones con un nuevo progtwig, reemplazando el que fue clonado).

Básicamente, su respuesta está oculta en esa explicación: cuando tiene un proceso con muchos subprocesos de LWP y organiza el proceso, tendrá dos procesos independientes con muchos subprocesos, que se ejecutarán simultáneamente.

Este truco es incluso útil: en muchos progtwigs, usted tiene un proceso principal que puede tener muchos hilos, algunos de los cuales incluyen procesos secundarios nuevos. (Por ejemplo, un servidor HTTP podría hacer eso: cada conexión al puerto 80 es manejada por un hilo, y luego se podría bifurcar un proceso secundario para algo como un progtwig CGI; luego se llamaría a exec (2) para ejecutar el progtwig CGI en lugar del proceso principal cerrado).

Siempre que acceda rápidamente a exec o a _exit en el proceso hijo bifurcado, estará bien en la práctica.

Es posible que desee utilizar posix_spawn () en su lugar, que probablemente hará lo correcto.

Si está utilizando la llamada al sistema unix ‘fork ()’, entonces técnicamente no está usando hilos, está usando procesos, ellos tendrán su propio espacio de memoria, y por lo tanto no pueden interferir entre sí.

Siempre que cada proceso utilice diferentes archivos, no debería haber ningún problema.