c ++ lambda callback para desencadenar evento

He estado tratando de envolver mi cabeza en torno a la funcionalidad de callback en c ++. Lo que estoy tratando de lograr es lo siguiente:

Tengo dos objetos, cada uno con su propio hilo. Un objeto A tiene un puntero al segundo objeto B Ver ejemplo:

 class A { public: // ... private: std::unique_ptr b; }; class B { public: void add_message(MessageType msg); // ... }; 

Lo que estoy tratando de lograr es tener el objeto A agregando un mensaje usando el puntero a B y luego continuar haciendo otras cosas, pero teniendo una callback o un controlador o algo que se activa cuando B tiene una respuesta a ese mensaje. B realiza algún procesamiento con el mensaje y puede pasarlo a otros objetos para procesarlo en su propio hilo, pero eventualmente aparecerá una respuesta. Entonces, ¿cómo puedo saber cuándo B tiene una respuesta a mi mensaje, por ejemplo:

 // In class A MessageType m(); b->add_message(m) // A's thread continues doing other stuff ... // some notification that b has a reply? 

Sé que podría tener que usar la función std :: para una callback que me gustaría usar, pero no puedo darme cuenta de cómo hacer esto exactamente mirando muchos ejemplos. Cualquier ayuda es apreciada y tenga en cuenta que he visto muchos ejemplos pero no puedo vincularlo con lo que estoy tratando de lograr o no estoy entendiendo …

Los hilos son secuencias de ejecución. Se comportan más o menos como progtwigs lineales de C ++, integrados en un modelo de memoria que les permite comunicarse y notar los cambios de estado causados ​​por otros subprocesos de ejecución.

Una callback a un hilo no puede hacerse cargo de una secuencia de ejecución sin la cooperación del hilo. El hilo que desea notificar debe verificar explícitamente si ha llegado un mensaje y procesarlo.


Hay dos formas comunes de manejar las respuestas a los mensajes.

El primero es un método std::future like. En él, la persona que llama recibe un token de algún tipo, y ese token representa la respuesta que puede o se producirá en el futuro.

El segundo es simplemente usar mensajes de nuevo. Usted envía un mensaje a B solicitando una respuesta. B envía un mensaje a A que contiene la respuesta. De la misma manera que B recibe los mensajes, A recibe los mensajes de vuelta. El mensaje puede contener un “objective de retorno” de algún tipo para ayudar a A a vincularlo con el mensaje original.

En un sistema basado en mensajes, es común tener un “bucle de eventos”. En lugar de un progtwig grande y lineal, tienes un hilo que vuelve repetidamente al “bucle de eventos”. Allí verifica la cola de mensajes, y si no hay, espera algunos.

Las tareas deben dividirse en porciones de tamaño de mordida en un sistema de este tipo, de modo que usted revise el bucle de eventos con la frecuencia suficiente para responder.

Una forma de hacer esto es con coroutines, un estado de ejecución sin poseer su propio ejecutor (como un hilo, que posee ambos). Coroutines periódicamente renunciar a la prioridad y “guardar su estado para más adelante”.


La solución futura es a menudo la más fácil, pero se basa en que A verifique periódicamente la respuesta.

Primero, un threaded_queue , que permite a cualquier número de productores y consumidores pasar cosas a la cola y comerlas de frente:

 template struct threaded_queue { using lock = std::unique_lock; void push_back( T t ) { { lock l(m); data.push_back(std::move(t)); } cv.notify_one(); } boost::optional pop_front() { lock l(m); cv.wait(l, [this]{ return abort || !data.empty(); } ); if (abort) return {}; auto r = std::move(data.back()); data.pop_back(); return std::move(r); } void terminate() { { lock l(m); abort = true; data.clear(); } cv.notify_all(); } ~threaded_queue() { terminate(); } private: std::mutex m; std::deque data; std::condition_variable cv; bool abort = false; }; 

Ahora, queremos pasar las tareas a una cola de este tipo y hacer que el que pasa la tarea recupere un resultado. Aquí es un uso de lo anterior con las tareas empaquetadas:

 template struct threaded_task_queue { threaded_task_queue() = default; threaded_task_queue( threaded_task_queue&& ) = delete; threaded_task_queue& operator=( threaded_task_queue&& ) = delete; ~threaded_task_queue() = default; template> std::future queue_task( F task ) { std::packaged_task p(std::move(task)); auto r = p.get_future(); tasks.push_back( std::move(p) ); return r; } void terminate() { tasks.terminate(); } std::function pop_task() { auto task = tasks.pop_front(); if (!task) return {}; auto task_ptr = std::make_shared>(std::move(*task)); return [task_ptr](Args...args){ (*task_ptr)(std::forward(args)...); }; } private: threaded_queue> tasks; }; 

Si lo hice bien, funciona así:

  • A envía colas una tarea a B en forma de un lambda. Esta lambda toma un conjunto fijo de argumentos (proporcionado por B) y devuelve algún valor.

  • B abre la cola y obtiene una std::function que toma los argumentos. Lo invoca; devuelve void en el contexto de B.

  • A recibió un future cuando puso en cola la tarea. Puede consultar esto para ver si está terminado.

Notarás que A no puede ser “notificado” que las cosas están hechas. Eso requiere una solución diferente. Pero si A llega a un punto en el que no puede progresar sin esperar el resultado de B, este sistema funciona.

Por otro lado, si A acumula una gran cantidad de mensajes de este tipo y, en ocasiones, necesita esperar la entrada de muchos de estos Bs hasta que alguno de ellos devuelva datos (o el usuario haga algo), necesita algo más avanzado que un std::future . El patrón general, que tiene un token que representa la computación futura que se entregará, es sólido. Pero debe boostlo para que funcione bien con múltiples fonts de cómputos y bucles de mensajes futuros y similares.

Código no probado.

Un enfoque para threaded_task_queue cuando está enviando mensajes es:

 template struct message_queue; template struct message_queue > { std::future queue_message(Args...args) { return this->queue_task( [tup = std::make_tuple(std::forward(args)...)] ( std::function f ) mutable { return std::apply( f, std::move(tup) ); } ); } bool consume_message( std::function f ) { auto task = pop_task(); if (!task) return false; task( std::move(f) ); return true; } }; 

en el lado del proveedor, proporciona Args... y en el lado del consumidor consume Args... y devuelve R , y en el lado del proveedor tiene un future para obtener el resultado una vez que el consumidor haya terminado.

Esto puede ser más natural que el threaded_task_queue que escribí.

std::apply es C ++ 17, pero hay implementaciones en la naturaleza para C ++ 11 y C ++ 14.