Las implementaciones actuales "sin bloqueo" siguen el mismo patrón la mayor parte del tiempo:
- * leer algún estado y hacer una copia **
- * modificar copia **
- hacer una operación enclavada
- reintentar si falla
(* opcional: depende de la estructura / algoritmo de los datos)
El último bit es inquietantemente similar a un spinlock. De hecho, es un spinlock básico . :)
Estoy de acuerdo con @nobugz en esto: el costo de las operaciones entrelazadas utilizadas en el multihilo sin bloqueo está dominado por la caché y las tareas de coherencia de memoria que debe realizar .
Sin embargo, lo que gana con una estructura de datos que está "libre de bloqueos" es que sus "bloqueos" son muy finos . Esto reduce la posibilidad de que dos subprocesos simultáneos accedan al mismo "bloqueo" (ubicación de memoria).
El truco la mayoría de las veces es que no tiene bloqueos dedicados; en su lugar, trata, por ejemplo, todos los elementos de una matriz o todos los nodos de una lista vinculada como un "bloqueo de giro". Usted lee, modifica e intenta actualizar si no hubo ninguna actualización desde su última lectura. Si lo hubo, vuelva a intentarlo.
Esto hace que su "bloqueo" (oh, lo siento, no bloqueo :) es muy fino, sin introducir requisitos adicionales de memoria o recursos.
Hacerlo más detallado disminuye la probabilidad de esperas. Hacerlo lo más detallado posible sin introducir requisitos de recursos adicionales suena genial, ¿no es así?
Sin embargo, la mayor parte de la diversión puede provenir de garantizar la correcta carga / pedido en la tienda .
Contrariamente a las intuiciones, las CPU son libres de reordenar las lecturas / escrituras de la memoria; por cierto, son muy inteligentes: le resultará difícil observar esto desde un solo hilo. Sin embargo, se encontrará con problemas cuando comience a realizar subprocesos múltiples en varios núcleos. Sus intuiciones se romperán: el hecho de que una instrucción sea anterior en su código no significa que realmente sucederá antes. Las CPU pueden procesar instrucciones fuera de orden: y les gusta especialmente hacer esto con instrucciones con acceso a la memoria, para ocultar la latencia de la memoria principal y hacer un mejor uso de su caché.
Ahora, contra la intuición, es seguro que una secuencia de código no fluye "de arriba hacia abajo", sino que se ejecuta como si no hubiera ninguna secuencia en absoluto, y puede llamarse "campo de juego del diablo". Creo que no es factible dar una respuesta exacta sobre qué pedidos de carga / tienda se realizarán. En cambio, uno siempre habla en términos de mays y mights y latas y se prepara para lo peor. "Oh, la CPU podría reordenar esta lectura para que venga antes de la escritura, por lo que es mejor colocar una barrera de memoria aquí, en este lugar".
La situación se complica por el hecho de que incluso estos mays y mights pueden diferir a través de arquitecturas de CPU. Que podría ser el caso, por ejemplo, que algo que está garantizado que no ocurrirá en una arquitectura que podría ocurrir en otro.
Para obtener un subproceso múltiple "sin bloqueo", debe comprender los modelos de memoria.
Sin embargo, lograr que elMFENCE
modelo de memoria y las garantías sean correctos no es trivial, como lo demuestra esta historia, en la que Intel y AMD hicieron algunas correcciones a la documentación que causaron cierto revuelo entre los desarrolladores de JVM . Al final resultó que, la documentación en la que los desarrolladores confiaron desde el principio no era tan precisa en primer lugar.
Los bloqueos en .NET dan como resultado una barrera de memoria implícita, por lo que está seguro al usarlos (la mayoría de las veces, es decir ... vea, por ejemplo, esta grandeza de Joe Duffy - Brad Abrams - Vance Morrison en inicialización lenta, bloqueos, volátiles y memoria barreras. :) (Asegúrese de seguir los enlaces en esa página).
Como ventaja adicional, se le presentará el modelo de memoria .NET en una misión secundaria . :)
También hay un "viejo pero dorado" de Vance Morrison: Lo que todo desarrollador debe saber sobre las aplicaciones multiproceso .
... y por supuesto, como mencionó @Eric , Joe Duffy es una lectura definitiva sobre el tema.
Un buen STM puede acercarse lo más posible al bloqueo de grano fino y probablemente proporcionará un rendimiento cercano o a la par con una implementación hecha a mano. Uno de ellos es STM.NET de los proyectos DevLabs de MS.
Si no eres un fanático de .NET, Doug Lea hizo un gran trabajo en JSR-166 .
Cliff Click tiene una versión interesante de las tablas hash que no se basa en la creación de bandas de bloqueo, como lo hacen las tablas hash concurrentes de Java y .NET, y parece escalar bien a 750 CPU.
Si no tiene miedo de aventurarse en el territorio de Linux, el siguiente artículo proporciona más información sobre los aspectos internos de las arquitecturas de memoria actuales y cómo el intercambio de líneas de caché puede destruir el rendimiento: Lo que todo programador debe saber sobre la memoria .
@Ben hizo muchos comentarios sobre MPI: Estoy de acuerdo sinceramente en que MPI puede brillar en algunas áreas. Una solución basada en MPI puede ser más fácil de razonar, más fácil de implementar y menos propensa a errores que una implementación de bloqueo a medias que intenta ser inteligente. (Sin embargo, subjetivamente, también es cierto para una solución basada en STM.) También apostaría a que es años luz más fácil escribir correctamente una aplicación distribuida decente en, por ejemplo, Erlang, como sugieren muchos ejemplos exitosos.
MPI, sin embargo, tiene sus propios costos y sus propios problemas cuando se ejecuta en un único sistema de múltiples núcleos . Por ejemplo, en Erlang, hay problemas que resolver en torno a la sincronización de la programación de procesos y las colas de mensajes .
Además, en su esencia, los sistemas MPI generalmente implementan una especie de programación N: M cooperativa para "procesos ligeros". Esto, por ejemplo, significa que hay un cambio de contexto inevitable entre procesos ligeros. Es cierto que no es un "cambio de contexto clásico", sino principalmente una operación de espacio de usuario y se puede hacer rápido; sin embargo, dudo sinceramente que pueda llevarse a los 20-200 ciclos que requiere una operación entrelazada . El cambio de contexto en modo de usuario es ciertamente más lentoincluso en la biblioteca Intel McRT. La programación N: M con procesos ligeros no es nueva. Los LWP estuvieron presentes en Solaris durante mucho tiempo. Fueron abandonados. Había fibras en NT. En su mayoría son ahora una reliquia. Hubo "activaciones" en NetBSD. Fueron abandonados. Linux tenía su propia opinión sobre el tema de los subprocesos N: M. Parece estar algo muerto a estas alturas.
De vez en cuando, hay nuevos competidores: por ejemplo, McRT de Intel , o más recientemente User-Mode Scheduling junto con ConCRT de Microsoft.
En el nivel más bajo, hacen lo que hace un programador MPI N: M. Erlang, o cualquier sistema MPI, podría beneficiarse enormemente de los sistemas SMP al explotar el nuevo UMS .
Supongo que la pregunta del OP no es sobre los méritos y los argumentos subjetivos a favor / en contra de cualquier solución, pero si tuviera que responder eso, supongo que depende de la tarea: para construir estructuras de datos básicas de bajo nivel y alto rendimiento que se ejecutan en un Un solo sistema con muchos núcleos , ya sea técnicas de bloqueo bajo / "sin bloqueo" o un STM producirá los mejores resultados en términos de rendimiento y probablemente superaría a una solución MPI en cualquier momento en cuanto al rendimiento, incluso si se eliminan las arrugas anteriores. por ejemplo, en Erlang.
Para construir algo moderadamente más complejo que se ejecute en un solo sistema, quizás elegiría el bloqueo de grano grueso clásico o, si el rendimiento es una gran preocupación, un STM.
Para construir un sistema distribuido, un sistema MPI probablemente sería una elección natural.
Tenga en cuenta que también hay implementaciones de MPI para .NET (aunque parecen no estar tan activas).