2010-11-04 27 views
9

Aquí está el código:IAsyncResult.AsyncWaitHandle.WaitOne() finaliza por delante de devolución de llamada

class LongOp 
{ 
    //The delegate 
    Action longOpDelegate = LongOp.DoLongOp; 
    //The result 
    string longOpResult = null; 

    //The Main Method 
    public string CallLongOp() 
    { 
     //Call the asynchronous operation 
     IAsyncResult result = longOpDelegate.BeginInvoke(Callback, null); 

     //Wait for it to complete 
     result.AsyncWaitHandle.WaitOne(); 

     //return result saved in Callback 
     return longOpResult; 
    } 

    //The long operation 
    static void DoLongOp() 
    { 
     Thread.Sleep(5000); 
    } 

    //The Callback 
    void Callback(IAsyncResult result) 
    { 
     longOpResult = "Completed"; 
     this.longOpDelegate.EndInvoke(result); 
    } 
} 

Aquí es el caso de prueba:

[TestMethod] 
public void TestBeginInvoke() 
{ 
    var longOp = new LongOp(); 
    var result = longOp.CallLongOp(); 

    //This can fail 
    Assert.IsNotNull(result); 
} 

Si esto se ejecute el caso de prueba puede fallar. ¿Por qué exactamente?

Hay muy poca documentación sobre cómo funciona delegate.BeginInvoke. ¿Alguien tiene alguna idea que les gustaría compartir?

Actualización Esta es una sutil condición de carrera que no está bien documentada en MSDN ni en ningún otro lado. El problema, como se explica en la respuesta aceptada, es que cuando la operación finaliza, se señala el Identificador de Espera y luego se ejecuta la Devolución de Llamada. La señal libera el hilo principal en espera y ahora la ejecución de devolución de llamada entra en la "carrera". Jeffry Richter's suggested implementation muestra lo que sucede detrás de las escenas:

// If the event exists, set it 
    if (m_AsyncWaitHandle != null) m_AsyncWaitHandle.Set(); 

    // If a callback method was set, call it 
    if (m_AsyncCallback != null) m_AsyncCallback(this); 

Para una solución refieren a la respuesta de Ben Voigt. Esa implementación no implica la sobrecarga adicional de un segundo identificador de espera.

+0

Elimine la devolución de llamada y vuelva a intentarlo. – jgauffin

+0

@jgauffin, si nota que la pregunta no es "¿Cómo hago para que funcione?" Claramente este es un ejemplo artificial. –

+0

Su pregunta es: "Si esto se ejecuta, el caso de prueba puede fallar. ¿Por qué exactamente?". Yo * sí * respondí eso. Porque intenta mezclar dos formas muy diferentes de manejar una operación asincrónica. – jgauffin

Respuesta

8

El ASyncWaitHandle.WaitOne() se indica cuando finaliza la operación asincrónica. Al mismo tiempo se llama a CallBack().

Esto significa que el código después de WaitOne() se ejecuta en el hilo principal y el CallBack se ejecuta en otro hilo (probablemente el mismo que ejecuta DoLongOp()). Esto da como resultado una condición de carrera donde el valor de longOpResult es esencialmente desconocido en el momento en que se devuelve.

Uno podría haber esperado que ASyncWaitHandle.WaitOne() habría sido señalado cuando la devolución de llamada se terminó, pero eso no es sólo cómo funciona ;-)

que necesitará otra ManualResetEvent tener el hilo principal espere a que CallBack establezca longOpResult.

0

La devolución de llamada se ejecuta después del método CallLongOp. Como solo configura el valor de la variable en la devolución de llamada, es razonable pensar que sería nulo. Lea esto: link text

+0

Es decir, el resultado que está buscando todavía no se ha establecido como callback si no se llama hasta que devuelve el método CallLongOp. – Kell

+0

gracias por su respuesta. La devolución de llamada no siempre se ejecuta después del método CallLongOp. Intenta poner Thread.Sleep (500); en CallLongOp antes de devolver longOpResult; y la prueba pasará –

3

Lo que está sucediendo

Desde su operación DoLongOp ha completado, se reanuda de control dentro CallLongOp y la función se completa antes de que haya finalizado la operación de devolución de llamada. Assert.IsNotNull(result); luego se ejecuta antes de longOpResult = "Completed";.

¿Por qué? AsyncWaitHandle.WaitOne() esperará solamente para su operación asincrónica para completar, no su devolución de llamada

El parámetro de devolución de llamada de BeginInvoke es en realidad un AsyncCallback delegate, lo que significa que su devolución de llamada asincrónica. Esto es por diseño, ya que el objetivo es procesar los resultados de la operación de forma asíncrona (y es el propósito de este parámetro de devolución de llamada).

Dado que la función BeginInvoke en realidad llama a su función de devolución de llamada, la llamada IAsyncResult.WaitOne es solo para la operación y no influye en la devolución de llamada.

Consulte el Microsoft documentation (sección Ejecución de un método de devolución de llamada cuando se completa una llamada asíncrona). También hay una buena explicación y ejemplo.

Si el hilo que inicia la llamada asincrónica no necesita ser el hilo que procesa los resultados, puede ejecutar un método de devolución de llamada cuando finaliza la llamada. El método de devolución de llamada se ejecuta en un hilo de ThreadPool.

Solución

Si desea esperar a que tanto la operación como la devolución de llamada, lo que necesita para manejar la señalización a sí mismo. Una forma de hacerlo es ManualReset, que sin duda le brinda el mayor control (y así es como lo ha hecho Microsoft en sus documentos).

Aquí se modifica el código utilizando ManualResetEvent.

public class LongOp 
{ 
    //The delegate 
    Action longOpDelegate = LongOp.DoLongOp; 
    //The result 
    public string longOpResult = null; 

    // Declare a manual reset at module level so it can be 
    // handled from both your callback and your called method 
    ManualResetEvent waiter; 

    //The Main Method 
    public string CallLongOp() 
    { 
     // Set a manual reset which you can reset within your callback 
     waiter = new ManualResetEvent(false); 

     //Call the asynchronous operation 
     IAsyncResult result = longOpDelegate.BeginInvoke(Callback, null);  

     // Wait 
     waiter.WaitOne(); 

     //return result saved in Callback 
     return longOpResult; 
    } 

    //The long operation 
    static void DoLongOp() 
    { 
     Thread.Sleep(5000); 
    } 

    //The Callback 
    void Callback(IAsyncResult result) 
    { 
     longOpResult = "Completed"; 
     this.longOpDelegate.EndInvoke(result); 

     waiter.Set(); 
    } 
} 

Para el ejemplo que ha dado, que sería mejor no utilizar una devolución de llamada y en lugar de manipular el resultado en su función CallLongOp, en cuyo caso su WaitOne en el delegado operación no tendrán ningún problema.

+0

gracias por la respuesta. Entonces, ¿qué hacemos exactamente con IAsyncResult que obtenemos de BeginInvoke()? –

+0

Puede usarlo para detener la ejecución en el método que llamó a begininvoke. Es decir. Cualquier escenario en el que desee esperar a que finalice la operación en sí. – badbod99

+0

¡CONDICIÓN DE CARRERA! Será mejor que cree el evento antes de llamar a 'BeginInvoke', pero agregar más objetos de sincronización es innecesario e ineficiente. –

5

Como han dicho otros, result.WaitOne solo significa que el objetivo de BeginInvoke ha finalizado, y no la devolución de llamada. Así que simplemente coloque el código de postprocesamiento en el delegado BeginInvoke.

//Call the asynchronous operation 
    Action callAndProcess = delegate { longOpDelegate(); Callafter(); }; 
    IAsyncResult result = callAndProcess.BeginInvoke(r => callAndProcess.EndInvoke(r), null); 


    //Wait for it to complete 
    result.AsyncWaitHandle.WaitOne(); 

    //return result saved in Callafter 
    return longOpResult; 
+0

Ok ... muy buena solución de hecho! Pero para una explicación de por qué creo que lo he cubierto bastante bien. – badbod99

+0

Es inteligente, pero ¿cuándo sería realmente útil? ManualReset le da control para esperar cada vez que desee esperar, esto llama a la operación y luego a una devolución de llamada para manejar el resultado y espera a la vez. Simplemente podría manejar el resultado en la operación en sí, si eso es lo que quería. – badbod99

+0

@ badbod99: Esto le permite manejar el resultado incluso si no escribió la función rellena en 'longOpDelegate' (o es un método de otra clase, y no tiene acceso al miembro privado 'longOpResult', o no quiero introducir acoplamiento inverso, o ...). –

0

que tenían el mismo problema hace poco, y pensé otra manera de resolverlo, funcionó en mi caso. Básicamente, si el tiempo de espera no lo detiene, vuelva a verificar que la bandera se completó cuando se agotó el tiempo de Espera. En mi caso, se señala el identificador de espera antes de bloquear el hilo, y justo después de la condición if, por lo que vuelva a verificarlo después de que el tiempo de espera lo solucione.

while (!AsyncResult.IsCompleted) 
{ 
    if (AsyncWaitHandle.WaitOne(10000)) 
     break; 
} 
Cuestiones relacionadas