c multithreading thread-safety nim

Con seguridad, "preste" el bloque de memoria a otro hilo en C, suponiendo que no hay "acceso concurrente"



multithreading thread-safety (3)

Mi revisión de la documentación de C++11 y una redacción similar en C11:n1570.pdf me lleva a la siguiente comprensión.

Los datos se pueden utilizar de forma segura entre subprocesos, si se realiza algún tipo de sincronización cooperativa entre los subprocesos. Si hubiera una cola, que dentro de un mutex leyó un elemento de la cola, y si los elementos se agregaran a la cola mientras se mantenía el mutex, entonces la memoria legible en el segundo hilo sería la memoria que se había escrito en el primer hilo

Esto se debe a que el compilador y la infraestructura de CPU subyacente no tienen permitido organizar los efectos secundarios que pasan a través del pedido.

A partir del n1570

Una evaluación A entre subprocesos ocurre antes de una evaluación B si A se sincroniza con B, A está ordenada por dependencia antes de B, o, para alguna evaluación X:

- A se sincroniza con X y X se secuencia antes que B,

- A se secuencia antes de que X y X inter-thread pase antes de B, o

- Un inter-thread sucede antes de X y X inter-thread pasa antes de B

Entonces, para garantizar que la memoria visible en el nuevo hilo sea consistente, lo siguiente garantizaría los resultados.

  • A Mutex accediendo a la cerradura.
  • Una escritura enclavada en el productor + una lectura enclavada en el consumidor

La escritura enclavada hace que todas las operaciones anteriores en el subproceso A se secuencian y se vacíen en caché antes de que el subproceso B vea la lectura.

Después de que los datos se escriben en la cola para "otro procesamiento de subprocesos", el primer subproceso no puede (desbloqueado) modificar o leer de forma segura la memoria del objeto, hasta que sepa (a través de algún mecanismo) que el otro subproceso está Ya no accedemos a los datos. Solo verá los resultados correctos, si esto se hace a través de algún mecanismo de sincronización.

Tanto los estándares de C ++ como los de C están destinados a formalizar el comportamiento existente de los compiladores y las CPU. Entonces, aunque hay garantías menos formales en el uso de pthreads y estándares C99, se esperaría que fueran consistentes.

De tu ejemplo

Hilo A

int index = findFreeIndex(thread_A_buffer)

Esta línea es problemática, ya que no muestra ninguna primitiva de sincronización. Si el mecanismo para findFreeIndex solo se basa en la memoria escrita por el subproceso A, esto funcionará. Si el hilo B, o cualquier otro hilo modifica la memoria, es necesario un bloqueo adicional.

lock(m) p = &(thread_A_buffer[index]) signal() unlock(m)

Esto está cubierto por ...

15 Una evaluación A es ordenada por dependencia antes de una evaluación B si

- A realiza una operación de liberación en un objeto atómico M y, en otro hilo, B realiza una operación de consumo en M y lee un valor escrito por cualquier efecto secundario en la secuencia de liberación encabezada por A, o

- para algunas evaluaciones X, A es ordenada por dependencia antes de X y X lleva una dependencia a B.

y

18 Una evaluación A ocurre antes de una evaluación B si A está secuenciada antes de que B o A se entrelazen antes de B.

Las operaciones antes de la sincronización "suceden antes" de la sincronización, y se garantiza que serán visibles después de la sincronización en el otro subproceso.

El bloqueo (una adquisición), y el desbloqueo (una versión), aseguran que hay un orden estricto en la información en el hilo A que se completa y que es visible para B.

thread_A_buffer[index] = 42; // happens before

En el momento en que la memoria thread_A_buffer está visible en A, pero leerla en B provoca un comportamiento indefinido.

lock(m); // acquire

Aunque es necesario para el lanzamiento, no puedo ver ningún resultado de la adquisición.

p = &thread_A_buffer[index]; unlock(m);

Toda la secuencia de instrucciones de A ahora es visible para B (debido a su sincronización con m).

thread_A_buffer[index] = 42; << This happens before and ... p = &thread_A_buffer[index]; << carries a dependency into p unlock(m);

Todas las cosas en A ahora son visibles para B porque

Una evaluación Un interhilo sucede antes de una evaluación B si A se sincroniza con B, A se ordena según la dependencia antes de B o, para alguna evaluación X

- A se sincroniza con X y X se secuencia antes que B,

- A se secuencia antes de que X y X inter-thread pase antes de B, o

- Un inter-thread ocurre antes de X y X inter-thread pasa antes de B.

pointer p_a = null do: // sleep lock(m) p_a = p unlock(m) while (p_a != null)

Este código es completamente seguro, el valor leído en p_a se ordenará con el otro hilo y no será nulo después de la escritura sincronizada en el hilo b. Nuevamente, el bloqueo / desbloqueo causa un ordenamiento estricto que asegura que el valor de lectura será el valor escrito.

Todas las interacciones del hilo B están dentro de un bloqueo, por lo que, de nuevo, son completamente seguras.

Si A modificara el objeto después de haberle dado el objeto a B, entonces no funcionaría, a menos que hubiera más sincronización.

El problema

Quiero asignar memoria en un hilo y "prestar" el puntero a otro hilo para que pueda leer esa memoria.

Estoy usando un lenguaje de alto nivel que se traduce a C El lenguaje de alto nivel tiene subprocesos (de API de subprocesos no especificados, ya que es multiplataforma, ver más abajo) y admite primitivas de subprocesamiento múltiple de C estándar, como atomic-compare-exchange, pero no está realmente documentado (no hay ejemplos de uso). Las limitaciones de este lenguaje de alto nivel son:

  • Cada hilo ejecuta un bucle infinito de procesamiento de eventos.
  • Cada hilo tiene su propio montón local, gestionado por algún asignador personalizado.
  • Cada subproceso tiene una cola de mensajes de "entrada", que puede contener mensajes de cualquier número de otros subprocesos diferentes.
  • Las colas de paso de mensajes son:
    1. Para mensajes de tipo fijo.
    2. Utilizando copia

Ahora esto no es práctico para mensajes grandes (no quiero la copia) o de tamaño variable (creo que el tamaño de matriz es parte del tipo). Quiero enviar tales mensajes, y aquí está el resumen de cómo quiero lograrlo:

  • Un mensaje (ya sea una solicitud o una respuesta ) puede almacenar la "carga útil" en línea (copiado, límite fijo en el tamaño de los valores totales), o un puntero a los datos en el montón del remitente
  • El contenido del mensaje (datos en el montón del remitente) es propiedad del hilo de envío (asignar y gratis)
  • El subproceso de recepción envía un agradecimiento al subproceso de envío cuando finalizan con el contenido del mensaje.
  • Los subprocesos de "envío" no deben modificar el contenido del mensaje después de enviarlos, hasta que reciban (ack).
  • Nunca debe haber un acceso de lectura concurrente en la memoria que se está escribiendo, antes de que se realice la escritura. Esto debe ser garantizado por el flujo de trabajo de colas de mensajes.

Necesito saber cómo asegurar que esto funcione sin carreras de datos. Tengo entendido que necesito usar vallas de memoria , pero no estoy completamente seguro de cuál (ATOMIC_RELEASE, ...) y dónde está en el bucle (o si necesito alguna).

Consideraciones de portabilidad

Debido a que mi lenguaje de alto nivel debe ser multiplataforma, necesito la respuesta para trabajar en:

  • Linux, MacOS, y opcionalmente Android y iOS
    • usando primitivas pthreads para bloquear colas de mensajes: pthread_mutex_init y pthread_mutex_lock + pthread_mutex_unlock
  • Windows
    • EnterCriticalSection objetos de sección crítica para bloquear colas de mensajes: InitializeCriticalSection y EnterCriticalSection + LeaveCriticalSection

Si ayuda, estoy asumiendo las siguientes arquitecturas:

  • Arquitectura de PC Intel / AMD para Windows / Linux / MacOS (?).
  • desconocido (ARM?) para iOS y Android

Y usando los siguientes compiladores (puede asumir una versión "reciente" de todos ellos):

  • MSVC en Windows
  • clang en linux
  • Xcode en MacOS / iOS
  • CodeWorks para Android en Android

Hasta ahora solo he construido en Windows, pero cuando la aplicación está terminada, quiero transferirla a las otras plataformas con un mínimo de trabajo. Por lo tanto, estoy tratando de asegurar la compatibilidad multiplataforma desde el principio.

Intento de solucion

Aquí está mi flujo de trabajo asumido:

  1. Lea todos los mensajes de la cola hasta que esté vacío (solo bloquee si estaba totalmente vacío).
  2. ¿Llamar a alguna "valla de memoria" aquí?
  3. Lea el contenido de los mensajes (destino de los punteros en los mensajes) y procese los mensajes.
    • Si el mensaje es una "solicitud", se puede procesar y los nuevos mensajes se almacenan en el búfer como "respuestas".
    • Si el mensaje es una "respuesta", el contenido del mensaje de la "solicitud" original se puede liberar (solicitud implícita "ack").
    • Si el mensaje es una "respuesta", y en sí mismo contiene un puntero a "contenido de la respuesta" (en lugar de una "respuesta en línea"), también debe enviarse un "mensaje de respuesta".
  4. ¿Llamar a alguna "valla de memoria" aquí?
  5. Envíe todos los mensajes en búfer a las colas de mensajes apropiadas.

El código real es demasiado grande para publicar. Aquí se simplifica (solo lo suficiente para mostrar cómo se accede a la memoria compartida ) pseudocódigo utilizando un mutex (como las colas de mensajes):

static pointer p = null static mutex m = ... static thread_A_buffer = malloc(...) Thread-A: do: // Send pointer to data int index = findFreeIndex(thread_A_buffer) // Assume different value (not 42) every time thread_A_buffer[index] = 42 // Call some "memory fence" here (after writing, before sending)? lock(m) p = &(thread_A_buffer[index]) signal() unlock(m) // wait for processing // in reality, would wait for a second signal... pointer p_a = null do: // sleep lock(m) p_a = p unlock(m) while (p_a != null) // Free data thread_A_buffer[index] = 0 freeIndex(thread_A_buffer, index) while true Thread-B: while true: // wait for data pointer p_b = null while (p_b == null) lock(m) wait() p_b = p unlock(m) // Call some "memory fence" here (after receiving, before reading)? // process data print *p_b // say we are done lock(m) p = null // in reality, would send a second signal... unlock(m)

¿Funcionaría esta solución? Reformulando la pregunta, ¿Thread-B imprime "42"? ¿Siempre, en todas las plataformas y sistemas operativos considerados (pthreads y Windows CS)? ¿O debo agregar otras primitivas de subprocesamiento, como las cercas de memoria?

Investigación

He pasado horas mirando muchas preguntas de SO relacionadas y leí algunos artículos, pero todavía no estoy totalmente seguro. Basado en el comentario de @Art, probablemente no necesito hacer nada . Creo que esto se basa en esta declaración del estándar POSIX, 4.12 Sincronización de memoria:

[...] utilizando funciones que sincronizan la ejecución de subprocesos y también sincronizan la memoria con respecto a otros subprocesos. Las siguientes funciones sincronizan la memoria con respecto a otros hilos.

Mi problema es que esta oración no especifica claramente si significan "toda la memoria a la que se accede" o "solo la memoria a la que se accede entre bloqueo y desbloqueo". He leído a personas que defienden ambos casos, ¡e incluso algunos insinuaron que fue escrito de manera imprecisa a propósito, para dar a los implementadores del compilador más libertad en su implementación!

Además, esto se aplica a pthreads, pero también necesito saber cómo se aplica a los hilos de Windows.

Elegiré cualquier respuesta que, basada en citas / enlaces de una documentación estándar o de alguna otra fuente altamente confiable, demuestre que no necesito vallas o que muestre qué vallas necesito , en las configuraciones de plataforma mencionadas anteriormente, al menos para el caso Windows / Linux / MacOS. Si los subprocesos de Windows se comportan como los pthreads en este caso, también me gustaría un enlace / presupuesto para eso.

Las siguientes son algunas (de las mejores) preguntas / enlaces relacionados que leí, pero la presencia de información conflictiva me hace dudar de mi comprensión.


Si desea tener la independencia de la plataforma, entonces necesita usar varias concentraciones de os y c:

  1. Uso del bloqueo mutex y desbloqueo para sincronización.
  2. Uso de variable condicional para señal a otro hilo.
  3. el uso de la memoria del montón con el incremento de mantener cuando se asigna a otro subproceso y lo condena una vez que finaliza el acceso. Esto evitará el libre inválido.

También uso Nim para proyectos personales. Nim tiene un recolector de basura y debes evitarlo para las rutinas de manejo de memoria de tu hilo usando su invocación de C:

https://nim-lang.org/docs/backends.html

En Linux, el malloc usa mutexes internamente para evitar la corrupción del acceso concurrente. Creo que Windows hace lo mismo. Puede utilizar la memoria libremente, pero debe evitar múltiples colisiones "libres" o de acceso (debe garantizar que solo un hilo esté usando la memoria y podría "liberarla").

Usted mencionó que utiliza una implementación de pila personalizada. Probablemente se puede acceder a este montón desde otros subprocesos, pero debe verificar si esta biblioteca no hará un "libre" para un puntero que está siendo manejado por otro subproceso. Si esta implementación de pila personalizada es el recolector de basura de Nim, debe evitarla a toda costa y realizar una implementación en C personalizada del acceso a la memoria y utilizar la invocación de C de Nim para memoria malloc y gratuita.