¿Cómo lograr un comportamiento sin bloqueo, pero con bloqueo?

9 minutos de lectura

Estoy implementando una cola de consumidor único de productor único sin bloqueo para una aplicación de red intensiva. Tengo un montón de subprocesos de trabajo que reciben trabajo en sus propias colas separadas, que luego eliminan y procesan.

La eliminación de los bloqueos de estas colas ha mejorado en gran medida el rendimiento bajo carga alta, pero ya no bloquean cuando las colas están vacíaslo que a su vez hace que el uso de la CPU se dispare.

¿Cómo puedo hacer que un subproceso se bloquee de manera eficiente hasta que pueda sacar algo de la cola con éxito o se elimine/interrumpa?

  • Hola, ¿puedes compartir conmigo el rps (solicitud por segundo) que lograste usando el enfoque? Hice un tipo de trabajo similar (implementación de un servidor HTTP simple), así que estoy interesado en saberlo. No sé cómo contactar más allá de comentar aquí. Disculpa si te molesté.

    – Ayub

    28 de agosto de 2012 a las 6:05

  • @Ayub Performance estuvo bien. RPS no es una buena unidad para medir el rendimiento debido a diferentes configuraciones de hardware, etc. Rediseñé la aplicación para permitir que los subprocesos de trabajo funcionen de forma completamente aislada y la ganancia de rendimiento fue ~10x. Compartir menos datos fue realmente la clave.

    – prisa

    28 de agosto de 2012 a las 16:27

  • ¿Puede explicar por qué eligió un enfoque de una cola por trabajador? Suena bastante subóptimo para mí. El tiempo de ejecución de los trabajos en las colas es difícil de prever.

    – apilador de clases

    2 de diciembre de 2015 a las 9:29

Avatar de usuario de Jason
jason

Si está en Linux, considere usar un Futex. Proporciona el rendimiento de una implementación sin bloqueo mediante el uso de operaciones atómicas en lugar de llamadas al kernel como lo haría un mutex, pero si necesita establecer el proceso en inactivo debido a que alguna condición no es verdadera (es decir, bloqueo de contención), lo hará luego haga las llamadas al kernel apropiadas para poner el proceso en suspensión y reactivarlo en un evento futuro. Es básicamente como un semáforo muy rápido.

  • ¡Aclaración útil! Voté a favor de ambas respuestas relacionadas con Futex por ahora. Gracias.

    – prisa

    22 de mayo de 2011 a las 19:22

  • +1 para futex. Su rendimiento no es tan bueno como sin bloqueo, pero es lo suficientemente bueno y es la elección perfecta cuando el bloqueo mutex es demasiado. La API pthread mutex está usando futex debajo de las escenas.

    usuario405725

    15 de junio de 2011 a las 20:26


  • Mutexes en Linux se implementan con un breve cmpxchg giro para el caso de baja contención, y retrocediendo a un futex llamada. Realmente no entiendo por qué lo llamas sin bloqueo cuando implementa el bloqueo (mutex de espacio de usuario rápido: el origen del nombre).

    – strcat

    26 de diciembre de 2013 a las 21:46


  • Lo llamo sin bloqueo debido al caso de baja contención que es lo que “normalmente” ocurre a menos que esté bajo una carga alta…

    – Jasón

    1 de enero de 2014 a las 17:25

Avatar de usuario de Alexey Kukanov
Aleksey Kukanov

En Linux, futex se puede utilizar para bloquear un hilo. Pero ten en cuenta que Los futexes son complicados!

ACTUALIZACIÓN: las variables de condición son mucho más seguras de usar que futexes y son más portátiles. Sin embargo, una variable de condición se usa en combinación con un mutex, por lo que, estrictamente hablando, el resultado ya no estará libre de bloqueos. Sin embargo, si su objetivo principal es el rendimiento (y no la garantía del progreso global), y la parte bloqueada (es decir, una condición para verificar después de activar el subproceso) es pequeña, es posible que obtenga resultados satisfactorios sin necesidad de entrar en sutilezas de integrar futexes en el algoritmo.

  • Esto parece muy interesante. Exploraré esto en breve y volveré con mi veredicto. Gracias.

    – prisa

    22 de mayo de 2011 a las 19:19

  • Él futex la llamada es una implementación de bloqueo. La API mutex simplemente gira cmpxchg un poco y luego vuelve a caer futex (espacio de usuario rápido exclusión mutua).

    – strcat

    26 de diciembre de 2013 a las 21:49

Si está en Windows, no podrá usar futexes, pero Windows Vista tiene un mecanismo similar llamado Eventos con clave. Desafortunadamente, esto no es parte de la API publicada (es una API nativa de NTDLL), pero puede usarla siempre que acepte la advertencia de que podría cambiar en futuras versiones de Windows (y no es necesario que se ejecute en núcleos anteriores a Vista). Asegúrese de leer el artículo que vinculé arriba. Aquí hay un no probado boceto de cómo podría funcionar:

/* Interlocked SList queue using keyed event signaling */

struct queue {
    SLIST_HEADER slist;
    // Note: Multiple queues can (and should) share a keyed event handle
    HANDLE keyed_event;
    // Initial value: 0
    // Prior to blocking, the queue_pop function increments this to 1, then
    // rechecks the queue. If it finds an item, it attempts to compxchg back to
    // 0; if this fails, then it's racing with a push, and has to block
    LONG block_flag;
};

void init_queue(queue *qPtr) {
    NtCreateKeyedEvent(&qPtr->keyed_event, -1, NULL, 0);
    InitializeSListHead(&qPtr->slist);
    qPtr->blocking = 0;
}

void queue_push(queue *qPtr, SLIST_ENTRY *entry) {
    InterlockedPushEntrySList(&qPtr->slist, entry);

    // Transition block flag 1 -> 0. If this succeeds (block flag was 1), we
    // have committed to a keyed-event handshake
    LONG oldv = InterlockedCompareExchange(&qPtr->block_flag, 0, 1);
    if (oldv) {
        NtReleaseKeyedEvent(qPtr->keyed_event, (PVOID)qPtr, FALSE, NULL);
    }
}

SLIST_ENTRY *queue_pop(queue *qPtr) {
    SLIST_ENTRY *entry = InterlockedPopEntrySList(&qPtr->slist);
    if (entry)
        return entry; // fast path

    // Transition block flag 0 -> 1. We must recheck the queue after this point
    // in case we race with queue_push; however since ReleaseKeyedEvent
    // blocks until it is matched up with a wait, we must perform the wait if
    // queue_push sees us
    LONG oldv = InterlockedCompareExchange(&qPtr->block_flag, 1, 0);

    assert(oldv == 0);

    entry = InterlockedPopEntrySList(&qPtr->slist);
    if (entry) {
        // Try to abort
        oldv = InterlockedCompareExchange(&qPtr->block_flag, 0, 1);
        if (oldv == 1)
            return entry; // nobody saw us, we can just exit with the value
    }

    // Either we don't have an entry, or we are forced to wait because
    // queue_push saw our block flag. So do the wait
    NtWaitForKeyedEvent(qPtr->keyed_event, (PVOID)qPtr, FALSE, NULL);
    // block_flag has been reset by queue_push

    if (!entry)
        entry = InterlockedPopEntrySList(&qPtr->slist);
    assert(entry);

    return entry;
}

También podría usar un protocolo similar usando Delgado leer escribir cerraduras y Variables de condición, con una ruta rápida sin bloqueo. Estos son envoltorios sobre eventos con clave, por lo que pueden generar más gastos generales que usar eventos con clave directamente.

  • Sí, pero las respuestas de futex ya estaban tomadas y pensé que podría ser útil para que alguien más busque sobre el tema más adelante 🙂

    – bdonlan

    23 de mayo de 2011 a las 13:12

  • Para completar, esto es interesante: ocasionalmente desarrollo para Windows. 🙂

    – prisa

    23 de mayo de 2011 a las 14:29

  • +1, también los eventos con clave también funcionan bien en la versión anterior a Vista (inicialmente se implementaron como un retroceso global único para todos para una situación de interbloqueo fuera de control con secciones críticas de menos de 2k). La única diferencia entre 2k/XP y Vista/7/8 es un detalle de implementación (lista vinculada frente a hash) que hace que Vista y los KE posteriores sean mucho más eficientes si observa muchas ubicaciones de memoria con un solo identificador (no hay diferencia práctica para el 99 % de todas las aplicaciones).

    – Damon

    22 de agosto de 2013 a las 15:49

¿Has probado la espera condicional? Cuando la cola se vacía, simplemente comience a esperar un nuevo trabajo. El subproceso que pone trabajos en la cola debería disparar la señal. De esta manera, solo usa bloqueos cuando la cola está vacía.

https://computing.llnl.gov/tutorials/pthreads/#ConditionVariables

Puede hacer que un subproceso se duerma utilizando la función sigwait(). Puede activar el hilo con pthread_kill. Esto es mucho más rápido que las variables de condición.

  • 1) Proporcione una referencia para su afirmación de que un mecanismo basado en señales será “mucho más rápido” que una variable de condición. En el caso de que el subproceso tenga que despertarse, en ambos casos los aspectos de programación y almacenamiento en caché juegan un papel muy importante. 2) Proporcione un resumen de cómo manejar las condiciones de carrera y el caso de que el subproceso no entre en modo de suspensión, sino que seleccione una entrada de la cola no vacía. En la práctica, esta ruta rápida es el factor clave para la escalabilidad/rendimiento bajo carga. Y se está volviendo un poco complicado si tienes un concepto mixto.

    – apilador de clases

    2 de diciembre de 2015 a las 9:08

Avatar de usuario de Brendan Long
Brendan largo

Podrías agregar duerme mientras espera. Simplemente elija la mayor espera que esté dispuesto a tener, luego haga algo como esto (pseudocódigo porque no recuerdo la sintaxis de pthread):

WAIT_TIME = 100; // Set this to whatever you're happy with
while(loop_condition) {
   thing = get_from_queue()
   if(thing == null) {
       sleep(WAIT_TIME);
   } else {
       handle(thing);
   }
}

Incluso algo corto como una suspensión de 100 ms debería reducir significativamente el uso de la CPU. Sin embargo, no estoy seguro de en qué punto el cambio de contexto lo hará peor que esperar ocupado.

  • 1) Proporcione una referencia para su afirmación de que un mecanismo basado en señales será “mucho más rápido” que una variable de condición. En el caso de que el subproceso tenga que despertarse, en ambos casos los aspectos de programación y almacenamiento en caché juegan un papel muy importante. 2) Proporcione un resumen de cómo manejar las condiciones de carrera y el caso de que el subproceso no entre en modo de suspensión, sino que seleccione una entrada de la cola no vacía. En la práctica, esta ruta rápida es el factor clave para la escalabilidad/rendimiento bajo carga. Y se está volviendo un poco complicado si tienes un concepto mixto.

    – apilador de clases

    2 de diciembre de 2015 a las 9:08

¿Ha sido útil esta solución?