Почему этот тест AsyncCallback иногда терпит неудачу? - PullRequest
4 голосов
/ 13 октября 2011

У меня есть следующий класс, который пытается действовать как простая асинхронная операция:

public class AsyncLineWriter
{
    private delegate void SynchronousWriteLineDelegate(string message);
    private SynchronousWriteLineDelegate DoWriteLine;
    private void SynchronousWriteLine(string message)
    {
        Console.WriteLine(message);
    }
    public AsyncLineWriter()
    {
        DoWriteLine = new SynchronousWriteLineDelegate(SynchronousWriteLine);

    public IAsyncResult BeginWriteLine(string message, AsyncCallback callback, object state)
    {
        return DoWriteLine.BeginInvoke(message,callback,state);
    }
    public void EndWriteLine(IAsyncResult asyncResult)
    {
        DoWriteLine.EndInvoke(asyncResult);
    }
}

Следующий модульный тест периодически терпит неудачу, но я не понимаю, где находится состояние гонки:

[TestMethod]
public void Callback_is_called()
{
    // Arrange
    AsyncLineWriter lineWriter = new AsyncLineWriter();
    object state = new object();
    object callbackState = null;
    AsyncCallback callback = (r) =>
        {
            callbackState = r.AsyncState;
        };

    // Act
    IAsyncResult asyncResult = lineWriter.BeginWriteLine("test", callback, state);
    lineWriter.EndWriteLine(asyncResult);

    // Assert
    Assert.AreSame(state, callbackState);
}

Ответы [ 2 ]

3 голосов
/ 13 октября 2011

В этом шаблоне обратный вызов выполняется в потоке пула потоков, и вы должны вызывать EndInvoke из обратного вызова.

EndInvoke не ожидает завершения обратного вызова (потому что это может привести к тупику), поэтому у вас есть гонка между обратным вызовом и вашим методом тестирования.


РЕДАКТИРОВАТЬ: ручку ожидания можно установить до завершения обратного вызова. Попробуйте это:

[TestMethod]
public void Callback_is_called()
{
    // Arrange
    var lw = new AsyncLineWriter();

    object state = new object();
    object callbackState = null;

    var mre = new ManualResetEvent( false );

    AsyncCallback callback = r =>
        {
            callbackState = r.AsyncState;

            lw.EndWriteLine( r );

            mre.Set();
        };

    // Act
    var ar = lw.BeginWriteLine( "test", callback, state );
    mre.WaitOne();

    // Assert
    Assert.AreSame( state, callbackState );
}
3 голосов
/ 13 октября 2011

Как уже отмечалось, в случаях, когда тест завершается успешно, вам просто повезло, что потоки чередуются таким образом, что обратный вызов вызывается до того, как произойдет вызов EndInvoke. Правильный шаблон APM - это вызов EndWriteLine внутри обратного вызова, что означает, что вы должны передать AsyncLineWriter как часть состояния методу BeginInvoke.

РЕДАКТИРОВАТЬ: есть дополнительное осложнение, поскольку обратный вызов может произойти после того, как IAsyncResult WaitHandle сигнализируется. Так что это не значит, что обратный вызов не вызывается, он вызывается только после проверки. Это исправляет это:

AsyncLineWriter lineWriter = new AsyncLineWriter();
Object myState = new Object();
object[] state = new object[2];
state[0] = lineWriter;
state[1] = myState;
object callbackState = null;

ManualResetEvent evnt = new ManualResetEvent(false);

AsyncCallback callback = (r) =>
    {  
        Object[] arr = (Object[])r.AsyncState;
        LineWriter lw = (LineWriter)arr[0];
        Object st = arr[1];
        callbackState = st;
        lw.EndWriteLine(r);
        evnt.Set();
    };

// Act
IAsyncResult asyncResult = lineWriter.BeginWriteLine("test", callback, state);

//asyncResult.AsyncWaitHandle.WaitOne(); -- callback can still happen after this!

evnt.WaitOne();

//Assert
Assert.AreSame(myState, callbackState);
...