En C ++ 20, obtuvimos la capacidad de dormir en variables atómicas, esperando que cambie su valor. Lo hacemos usando el método std::atomic::wait .
Desafortunadamente, mientras que la wait se ha estandarizado, wait_for y wait_until no lo están. Lo que significa que no podemos dormir en una variable atómica con un tiempo de espera.
De todos modos, dormir en una variable atómica se implementa entre bastidores con WaitOnAddress en Windows y la llamada al sistema futex en Linux.
Solucionando el problema anterior (no hay forma de dormir en una variable atómica con un tiempo de espera), podría pasar la dirección de memoria de un std::atomic a WaitOnAddress en Windows y funcionará (más o menos) sin UB, ya que la función obtiene void* como parámetro, y es válido convertir std::atomic<type> a void*
En Linux, no está claro si está bien mezclar std::atomic con futex . futex obtiene uint32_t* o int32_t* (dependiendo del manual que lea), y convertir std::atomic<u/int> a u/int* es UB. Por otro lado, el manual dice
El argumento uaddr apunta a la palabra futex. En todas las plataformas, los futexes son números enteros de cuatro bytes que se deben alinear en un límite de cuatro bytes . La operación a realizar en el futex se especifica en el argumento futex_op; val es un valor cuyo significado y propósito depende de futex_op.
Sugiriendo que alignas(4) std::atomic<int> debería funcionar, y no importa qué tipo de entero sea, siempre que el tipo tenga el tamaño de 4 bytes y la alineación de 4.
Además, he visto muchos lugares donde se implementa este truco de combinar atomics y futexes, incluidos boost y TBB .
Entonces, ¿cuál es la mejor manera de dormir en una variable atómica con un tiempo de espera sin UB? ¿Tenemos que implementar nuestra propia clase atómica con primitivas del sistema operativo para lograrlo correctamente?
(Existen soluciones como mezclar atómicos y variables de condición, pero no son óptimas)
No necesariamente debería tener que implementar una API atomic personalizada completa, en realidad debería ser seguro simplemente extraer un puntero a los datos subyacentes del atomic<T> y pasarlo al sistema.
Dado que std::atomic no ofrece algún equivalente de native_handle como ofrecen otras primitivas de sincronización, se verá atascado haciendo algunos trucos específicos de implementación para intentar que interactúe con la API nativa.
En su mayor parte, es razonablemente seguro asumir que el primer miembro de estos tipos en las implementaciones será el mismo que el tipo T , al menos para los valores integrales [1] . Esta es una garantía que permitirá extraer este valor.
... y convertir
std::atomic<u/int>au/int*es UB
Este no es realmente el caso.
std::atomic está garantizado por el estándar para ser Standard-Layout Type . Una propiedad útil pero a menudo esotérica de los tipos de diseño estándar es que es seguro reinterpret_cast una T a un valor o referencia del primer subobjeto (por ejemplo, el primer miembro de std::atomic ).
Siempre que podamos garantizar que std::atomic<u/int> contiene solo u/int como miembro (o al menos, como su primer miembro), entonces es completamente seguro extraer el tipo de esta manera:
auto* r = reinterpret_cast<std::uint32_t*>(&atomic); // Pass to futex API... Este enfoque también debe mantenerse en las ventanas para convertir el atomic al tipo subyacente antes de pasarlo a la API void* .
Nota: pasar un puntero T* a un void* que se reinterpreta como una U* (como un atomic<T>* to void* cuando espera una T* ) es un comportamiento indefinido , incluso con garantías de diseño estándar (como que yo sepa). Es probable que aún funcione porque el compilador no puede ver las API del sistema, pero eso no hace que el código esté bien formado.
Nota 2: no puedo hablar sobre la API WaitOnAddress porque en realidad no la he usado, pero cualquier API atómica que dependa de la dirección de un valor integral correctamente alineado ( void* o de otro modo) debería funcionar correctamente extrayendo un puntero al valor subyacente.
[1] Dado que está etiquetado como C++20 , puede verificarlo con std::is_layout_compatible con static_assert :
static_assert(std::is_layout_compatible_v<int,std::atomic<int>>);(Gracias a @apmccartney por esta sugerencia en los comentarios).
Puedo confirmar que este será compatible con el diseño de STL , libc++ y libstdc++ de Microsoft; sin embargo, si no tiene acceso a is_layout_compatible y está utilizando un sistema diferente, es posible que desee verificar los encabezados de su compilador para asegurarse de que esta suposición se mantenga.