C ++ 11 Implementación de Spinlock usando el encabezado “

6 minutos de lectura

avatar de usuario de syko
siko

Implementé la clase SpinLock, como sigue

struct Node {
    int number;
    std::atomic_bool latch;

    void add() {
        lock();
        number++;
        unlock();
    }
    void lock() {
        bool unlatched = false;
        while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire));
    }
    void unlock() {
        latch.store(false , std::memory_order_release);
    }
};

Implementé la clase anterior e hice dos subprocesos que llaman al método add() de una misma instancia de la clase Node 10 millones de veces por subproceso.

el resultado es, lamentablemente, no 20 millones. ¿Que me estoy perdiendo aqui?

  • Tenga cuidado, este es el peor spinlock que posiblemente pueda implementar. Entre otros problemas: 1) Cuando finalmente adquieres el lockte llevas a la madre de todas las ramas mal vaticinadas dejando el while bucle, que es el peor momento posible para tal cosa. 2) El lock La función puede privar a otro subproceso que se ejecuta en el mismo núcleo virtual en una CPU hiperproceso.

    –David Schwartz

    20/04/2015 a las 10:50


  • @DavidSchwartz, Gracias por sus comentarios. ¿Puedo preguntar más sobre los problemas que mencionaste? 2). Esa función de bloqueo puede privar a otro hilo (¡así es!, y está destinado a hacer eso ya que estoy seguro de que la vida útil del bloqueo es muy corta). Puedo usar algunos contadores de giros para resolver este problema, ¿verdad? 1). ¿Por qué tomo ‘la madre de todos los brabches mal predicados’ en este código? ¿Y cómo puedo mejorarlo? ¿tienes algún comentario al respecto? Gracias de nuevo

    – syko

    26 de abril de 2015 a las 5:01


  • La pausa evita la ejecución especulativa, eliminando la penalización por error de predicción de rama. (Y, por cierto, si no conoce este tipo de cosas de adentro hacia afuera, no tiene por qué escribir spinlocks. Cometerá todos los errores y ni siquiera se dará cuenta de que tenía otras opciones).

    –David Schwartz

    27 de abril de 2015 a las 8:50


  • @DavidSchwartz, estoy interesado en escribir este tipo de cosas, pero no las conozco de adentro hacia afuera. ¿Puede recomendar cómo una persona puede hacerse saber esto de adentro hacia afuera?

    – tiro en código

    1 de junio de 2016 a las 0:22

  • @DavidSchwartz, parece que la única forma de adquirir el conocimiento suficiente para poder intentar escribir este tipo de código es intentar escribir este tipo de código.

    – tiro en código

    04/06/2016 a las 11:41

El problema es ese compare_exchange_weak actualiza el unlatched variable una vez que falla. De la documentación de compare_exchange_weak:

Compara el contenido del valor contenido del objeto atómico con el esperado: – si es verdadero, reemplaza el valor contenido con val (como store).
– si es falso, reemplaza esperado con el valor contenido.

Es decir, después de la primera falla compare_exchange_weak, unlatched se actualizará a truepor lo que la siguiente iteración del bucle intentará compare_exchange_weak true con true. Esto tuvo éxito y acaba de tomar un candado que estaba en manos de otro subproceso.

Solución: asegúrese de configurar unlatched de regreso false antes de cada compare_exchange_weakp.ej:

while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire)) {
    unlatched = false;
}

  • +1, y nota para el OP, el constructor predeterminado de Node voluntad no inicializar valor number tal como se presenta. Es decir Node node; no dejará un valor determinista en node.number para iniciar su secuencia. Necesitas un constructor de tal uso, Node() : number() {} es suficiente. verlo en vivo

    – WhozCraig

    27 de octubre de 2014 a las 8:34


  • ¡¡Gracias por tus comentarios!! ¡Y encontré mis errores en la piscina! gracias

    – syko

    27 de octubre de 2014 a las 8:42


  • tenga en cuenta que el bucle también requiere __mm_pause() para habilitar los hiperprocesos.

    – Antón

    27 de octubre de 2014 a las 8:57

Avatar de usuario de MikeMB
mikemb

Como menciona @gexicide, el problema es que el compare_exchange las funciones actualizan el expected variable con el valor actual de la variable atómica. Esa es también la razón por la que tienes que usar la variable local unlatched en primer lugar. Para solucionar esto puedes configurar unlatched vuelve a falso en cada iteración del bucle.

Sin embargo, en lugar de usar compare_exchange para algo para lo que su interfaz no es adecuada, es mucho más simple de usar std::atomic_flag en cambio:

class SpinLock {
    std::atomic_flag locked = ATOMIC_FLAG_INIT ;
public:
    void lock() {
        while (locked.test_and_set(std::memory_order_acquire)) { ; }
    }
    void unlock() {
        locked.clear(std::memory_order_release);
    }
};

Fuente: preferencia cp

Especificar manualmente el orden de la memoria es solo un ajuste menor de rendimiento potencial, que copié de la fuente. Si la simplicidad es más importante que el último bit de rendimiento, puede ceñirse a los valores predeterminados y simplemente llamar locked.test_and_set() / locked.clear().

Por cierto.: std::atomic_flag es el único tipo que está garantizado para estar libre de bloqueo, aunque no conozco ninguna plataforma, donde las operaciones en std::atomic_bool no están libres de bloqueo.

Actualizar: Como se explica en los comentarios de @David Schwartz, @Anton y @Technik Empire, el bucle vacío tiene algunos efectos no deseados, como predicción errónea de ramas, inanición de subprocesos en los procesadores HT y un consumo de energía demasiado alto; en resumen, es una forma bastante ineficiente de Espere. El impacto y la solución son específicos de la arquitectura, la plataforma y la aplicación. No soy un experto, pero la solución habitual parece ser agregar un cpu_relax() en linux o YieldProcessor() en las ventanas al cuerpo del bucle.

EDIT2: Para que quede claro: la versión portátil presentada aquí (sin las instrucciones especiales de cpu_relax, etc.) ya debería ser lo suficientemente buena para muchas aplicaciones. Si tu SpinLock gira mucho porque otra persona mantiene el bloqueo durante mucho tiempo (lo que ya podría indicar un problema general de diseño), probablemente sea mejor usar un mutex normal de todos modos.

  • simplemente agregue una llamada std::this_thread::yield en el ciclo while: es.cppreference.com/w/cpp/thread/yield

    – Martín Gerhardy

    6 de junio de 2016 a las 7:58

  • @Martin: Pensé en eso, pero std::this_thread::yield() es una llamada al sistema bastante pesada, así que no estoy seguro si la pondría en el cuerpo del bucle de un spinlock. Mi suposición (no probada) es que en la mayoría de las situaciones, donde eso estaría bien, querrás usar un std::mutex (o similar) para empezar.

    – Mike MB

    6 de junio de 2016 a las 8:23

  • @MartinGerhardy No tiene sentido usar el rendimiento en un bloqueo giratorio. El objetivo de los spin-locks es evitar cambios de contexto que son costosos en pequeñas secciones críticas.

    – Jorge Bellón

    5 de enero de 2017 a las 10:13


  • @rox ese no es mi punto. La definición técnica de spin mutex es un bloqueo que no libera la CPU si otros ya la han adquirido. En los sistemas de un solo núcleo, generalmente no usa spinlocks sino un mutex regular.

    – Jorge Bellón

    6 de diciembre de 2017 a las 17:32

  • @JorgeBellón Hasta donde yo sé, la implementación de glibc usa algo similar para producir: github.com/lattera/glibc/blob/master/mach/spin-solid.c . No estoy seguro de dónde encontraste la definición.

    – Rox

    7 de diciembre de 2017 a las 5:01

¿Ha sido útil esta solución?