Patrón de concurrencia de registrador en aplicación multiproceso

8

El contexto: estamos trabajando en una aplicación multiproceso (Linux-C) que sigue un modelo de canalización.

Cada módulo tiene un hilo privado y objetos encapsulados que procesan datos; y cada etapa tiene una forma estándar de intercambio de datos con la siguiente unidad.

La aplicación está libre de pérdidas de memoria y es segura para subprocesos utilizando bloqueos en el punto donde intercambian datos. El número total de hilos es de aproximadamente 15, y cada hilo puede tener de 1 a 4 objetos. Hacer alrededor de 25-30 objetos impares, todos los cuales tienen algunos registros críticos que hacer.

La mayoría de las discusiones que he visto sobre diferentes niveles, como en Log4J, y sus otras traducciones. La verdadera gran pregunta es sobre cómo debería suceder realmente el registro general.

Un enfoque es todo lo fprintfque hace el registro local stderr. El stderr se redirige a algún archivo. Este enfoque es muy malo cuando los registros se vuelven demasiado grandes.

Si todos los objetos crean instancias de sus registradores individuales (aproximadamente 30-40 de ellos) habrá demasiados archivos. Y a diferencia de lo anterior, uno no tendrá la idea del verdadero orden de los eventos. La marca de tiempo es una posibilidad, pero aún es un desastre clasificar.

Si hay un único patrón de registrador global (singleton), indirectamente bloquea tantos hilos mientras uno está ocupado colocando registros. Esto es inaceptable cuando el procesamiento de los hilos es pesado.

Entonces, ¿cuál debería ser la forma ideal de estructurar los objetos de registro? ¿Cuáles son algunas de las mejores prácticas en aplicaciones reales a gran escala?

¡También me encantaría aprender de algunos de los diseños reales de aplicaciones a gran escala para obtener inspiración!

======

EDITAR:

Basado en ambas respuestas, aquí está la pregunta que ahora me queda:

¿Cuál es la mejor práctica sobre la asignación de registradores (colas de registro) al objeto: si deben llamar a global_api () o si el registrador se les debe asignar en el constructor. Cuando los objetos están en una jerarquía profunda, este enfoque posterior se vuelve tedioso. Si están llamando a global_api (), es una especie de acoplamiento con la aplicación, por lo que intentar usar este objeto en otra aplicación arroja esta dependencia. ¿Hay una forma más limpia para esto?

Dipan Mehta
fuente
1
Hola, bienvenido de nuevo! Los hemos extrañado tanto aquí como en The Workplace.
yannis

Respuestas:

10

una forma aceptable de usar el registrador singleton que delega el registro real a su propio hilo

luego puede usar cualquier solución eficiente productor-consumidor (como una lista vinculada sin bloqueo basada en el CaS atómico) para recopilar los mensajes de registro sin preocuparse de que sea un bloqueo global implícito

la llamada de registro luego filtrará y construirá el mensaje de registro y luego lo pasará al consumidor, el consumidor lo tomará y lo escribirá (y liberará los recursos del mensaje individual)

monstruo de trinquete
fuente
Gracias por la respuesta. Internamente estaba pensando en la misma dirección. Mi única pregunta es: si desea hacer que un objeto tenga un propósito general, ¿no es limitante que acceda a un extern global esperado de la Aplicación? ¿Puede elaborar más sobre cómo se asignará por módulo y el registrador global?
Dipan Mehta
6

La respuesta de Ratchet Freak es lo que inicialmente pensé también.

Un método alternativo podría ser dar a cada uno de sus módulos su propia cola productor-consumidor y luego hacer que su mecanismo de registro escanee estas colas en su propio hilo.

Esto podría terminar siendo más flexible porque puede cambiar cuántos registradores usa: puede tener uno para todo, uno para cada módulo, o dividir los módulos en grupos y tener uno para cada grupo.

Editar: Elaboración

(No importa mi C, eso es lo que dijiste que estás codificando, ¿verdad?)

Entonces, esta idea es tener una lista / lista de productores y consumidores para cada uno de sus módulos. Tal cola probablemente sería algo como esto:

enum LogItemType {INFO, ERROR};

struct LogItem
{
    LogItemType type;
    char *msg;
};

struct LogQueue {...}; // implementation details -- holds an array/list of LogItem

bool queueLogItem(log_queue *, log_item *);
bool queueHasItems(log_queue *);
bool queueIsFull(log_queue *);
LogItem *dequeueLogItem(log_queue *);

Cada módulo deberá inicializar su propia cola o pasar uno por el código de inicialización que configura los subprocesos, etc. El código de inicio probablemente debería mantener referencias a todas las colas:

LogQueue *module_queues = {module_1_queue_ptr, module_2_queue_ptr, ... , module_N_queue_ptr};

setModuleLoggingQueue(module1, module_1_queue_ptr);
// .
// .
// .

Dentro de los módulos, los haría crear LogItems y ponerlos en cola para cada mensaje.

LogItem *item = malloc(sizeof(LogItem));
item->type = INFO;
item->msg = malloc(MSG_SIZE)
memcpy("MSG", item->msg);
queueLogItem(module_queue, item);

Luego, tendría uno o más consumidores de las colas que tomarían los mensajes y realmente escribirían el registro en un bucle principal de la siguiente manera:

void loggingThreadBody()
{
    while (true)
    {
        for (i = 0; i < N; i++)
        {
            if (queueHasItems(module_queues[i]))
                writeLogItem(dequeueLogItem(module_queues[i]));
        }

        threadSleep(200);
    }
}

o algo así.

Esto sería flexible al permitirle tener diferentes consumidores de las colas, por ejemplo:

// For one logger:

LogQueue *module_queues = {module_1_queue_ptr, module_2_queue_ptr, ... , module_N_queue_ptr};

initLoggerThread(module_queues);


// -OR-
// For multiple loggers:

LogQueue *group1_queues = {module_1_queue_ptr, ..., module_4_queue_ptr};
LogQueue *group2_queues = {module_5_queue_ptr, ... , module_Nmin7_queue_ptr};
LogQueue *group3_queues = {module_Nmin7_queue_ptr, ... , module_N_queue_ptr};

initLoggerThread(group1_queues);
initLoggerThread(group2_queues);
initLoggerThread(group3_queues);

Nota: Supongo que desearía asignar la memoria para un mensaje de registro en el tiempo de cola y desasignarlo en el tiempo de consumo (suponiendo que sus mensajes contengan contenido dinámico).

Otra edición:

Olvidé mencionar: si espera mucha actividad en los subprocesos de su módulo, es posible que vea si puede hacer las anotaciones cronológicas de forma asincrónica para que las cosas no se bloqueen.

Sin embargo, otra edición:

Probablemente también desee poner una marca de tiempo como parte del LogItem; con el (los) hilo (s) del registrador pasando por las colas secuencialmente, las declaraciones de registro pueden desordenarse desde que ocurrieron cronológicamente en los módulos.

Y otra edición más:

Con esta configuración, sería bastante fácil pasar todos los módulos a la misma cola y solo hacer que el consumidor mire esa cola, lo que lo llevaría de regreso a la respuesta de Ratchet Freak.

Geeze, ¿vas a dejar de editar?

También podría tener una cola para cada grupo de módulos y tener un registrador para cada cola.

Ok, me detendré ahora.

Pablo
fuente
Esto es interesante. ¿Puedes elaborar más de este diseño?
Dipan Mehta
@DipanMehta seguro, pero no puedo hacerlo en este momento. Intentaré actualizarlo esta noche, definitivamente mañana por la noche.
Paul
@DipanMehta Ok, he actualizado mi respuesta :)
Paul
@DipanMehta agregó otra edición ...
Paul
Gracias Paul. Esto parece mejor y realmente responde a mi pregunta. He agregado un pequeño comentario adicional en la pregunta: déjame ver si alguien puede arrojar luz sobre esto.
Dipan Mehta