usar react que promises promesas ejemplos cuando callbacks async c++ winapi timer

c++ - react - CreateTimerQueueTimer callback y condición de carrera



promises vs callbacks (3)

Estoy usando colas de temporizador en mi aplicación, y paso un puntero a uno de mis propios objetos Cimer Timer como el ''parámetro'' a la devolución de llamada (en CreateTimerQueueTimer). Luego invoco un método virtual en el objeto en la devolución de llamada.

El destructor del objeto Timer se asegurará de cancelar el temporizador usando DeleteTimerQueueTimer ().

static void callback( PVOID param, BOOLEAN timerOrWaitFired ) { Timer* timer = reinterpret_cast< Timer* >( param ); timer->TimedOut(); } class Timer { public: Timer(); virtual ~Timer() { ::DeleteTimerQueueTimer( handle ); } void Start( double period ) { ::CreateTimerQueueTimer( &handle, ..., &callback, this, ... ); } virtual void TimedOut() = 0; ... };

Sin embargo, existe una condición sutil de carrera: si la devolución de llamada ya se ha invocado, pero el objeto del temporizador se destruye antes de la llamada a TimedOut (), la aplicación se bloquea porque la devolución de llamada llama al método virtual en un objeto inexistente. O peor aún, mientras se elimina.

Tengo mutexes en el lugar para controlar las llamadas de subprocesos múltiples, pero sigo teniendo el problema.

¿Es realmente una buena idea usar un puntero de objeto como parámetro de devolución de llamada? Sin garantías de sincronización entre los hilos, me huele mal.

¿Hay una mejor solución? ¿Qué hacen otras personas?

Una cosa que ocurre es mantener un conjunto de punteros en cada instancia de Timer (agregar en constructor, eliminar en destructor). Pero no creo que esto funcione porque si se deriva Timer, solo eliminaríamos el puntero del conjunto en el destructor de la clase base; el daño ya está hecho si comenzamos a destruir el objeto derivado.

Aclamaciones.


El concepto de utilizar el puntero de objeto como parámetro de función de devolución de llamada no está mal por sí mismo. Sin embargo, obviamente necesita comenzar la destrucción después de que la última devolución de llamada haya salido.

Por lo tanto, no convertiría a Timer en abstracto y derivaría de ello. Utilizaría otra clase abstracta TimerImpl y haré que la clase Timer use una instancia TimerImpl :

class Timer { TimerInstance* impl; void TimeOut() { impl->TimeOut(); } public: ~Timer() { ... make sure the timer has ended and wont fire again after this line... delete impl; } } struct TimerImpl { virtual void TimeOut()=0; virtual ~TimerImpl(); }

De esta manera, puedes asegurarte de que la destrucción no comenzará hasta después de que digas.

Lo segundo es que tienes que esperar a que se apague el último evento del temporizador. De acuerdo con el documento de MSDN , puede hacerlo llamando

DeleteTimerQueueTimer(TimerQueue, Timer, INVALID_HANDLE_VALUE)


Es casi seguro que no puede hacer esto con un modelo de herencia. El principal problema es que para cuando se ha ingresado el constructor de la clase base, el objeto derivado ya no es válido, pero el temporizador puede dispararse y nada detiene el intento de llamada a la función virtual, lo que dará como resultado un comportamiento indefinido.

Creo que la manera de hacerlo es una envoltura como esta. El objetivo es garantizar que no exista una condición de carrera al tratar de enviar el evento de "tiempo de espera agotado".

Esta implementación todavía tiene un defecto. Existe la posibilidad de que el evento del temporizador esté esperando cuando el objeto del temporizador comience a borrarse. Es posible que el destructor libere el mutex y luego destruya el mutex mientras el hilo del temporizador está esperando en el mutex. Hemos evitado la carrera en el envío del evento ''tiempo de espera agotado'', pero el comportamiento de un hilo esperando en un mutex que se destruye depende de la implementación de mutex.

static void callback( PVOID param, BOOLEAN timerOrWaitFired ); class TimerWrapper { public: /* Take reference to std::auto_ptr to ensure ownership transfer is explicit */ TimerWrapper( std::auto_ptr<Timer>& timer ) : timer_(timer) { ::CreateTimerQueueTimer( &htimer_, ..., callback, this, ... ); } void TimedOut() { ScopedGuard guard( mutex_ ); if( timer_.get() ) timer_->TimedOut(); } ~TimerWrapper() { ::DeleteTimerQueueTimer( htimer_, ... ); ScopedGuard guard( mutex_ ); timer_.reset(); } private: Mutex mutex_; std::auto_ptr<Timer> timer_; HANDLE htimer_; }; static void callback( PVOID param, BOOLEAN timerOrWaitFired ) { TimerWrapper* timer = reinterpret_cast< TimerWrapper* >( param ); timer->TimedOut(); }


Cuando llame a DeleteTimerQueueTimer, asegúrese de pasar INVALID_HANDLE_VALUE para el evento de finalización. Esto bloqueará su destructor hasta que se completen o cancelen todas las devoluciones de llamada pendientes.

p.ej

virtual ~Timer() { ::DeleteTimerQueueTimer( timerQueue, handle, INVALID_HANDLE_VALUE ); }

Esto implica que su código se bloqueará hasta que se completen o cancelen todas las devoluciones de llamada del temporizador. A continuación, puede continuar con la destrucción, como de costumbre. Sin embargo, hay que tener en cuenta que no se puede llamar al eliminador de temporizador deletetimer desde la misma devolución de llamada del temporizador o se bloqueará.

Creo que esto solo debería ser suficiente para evitar la condición de carrera que estás experimentando.