为什么这个AsyncCallback测试有时会失败?

4

我有一个类,试图作为简单的异步操作:

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);
}

似乎回调函数不是异步操作的一部分,而EndInvoke正在等待它结束。 - dtb
为什么测试有时候会通过,有时候不行? - Jay Sullivan
因为有时这两个线程会以这样的方式交错。 - dtb
如果您能提供一个修复方案,使得这个测试通过并且仍然具有期望的行为(证明回调被调用),那么这将是一个好的答案。 - Jay Sullivan
2个回答

3
正如已经指出的那样,当测试成功时,你只是运气好,线程以这样的方式交错,使得回调在调用EndInvoke之前被调用。正确的 APM 模式是在回调中调用你的 EndWriteLine,这意味着你必须将 AsyncLineWriter 作为状态的一部分传递给 BeginInvoke 方法。
编辑:还有一个额外的复杂性,因为回调可能发生在 IAsyncResultWaitHandle 被信号化之后。所以不是回调没有被调用,而是在检查之后才被调用。这样就可以解决问题了。
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);

如果r是一个AsyncResult,您也可以使用AsyncResult.AsyncDelegate。+1 - Nick Butler
好的,看起来很有前途,但有时仍会失败。如果你不相信我,可以试一下。 - Jay Sullivan
好的,我确认了。我会扩展答案来解释。 - Steve Rowbotham

3
在这种模式下,回调在线程池线程上运行,您应该从回调内部调用EndInvokeEndInvoke不会等待回调完成(因为这会导致死锁),所以您的测试方法和回调之间存在竞争关系。
编辑:等待句柄也可以在回调完成之前设置。请尝试此操作:
[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 );
}

有趣的是,在回调完成之前,可以设置AsyncWaitHandle。这是有效的。 - Jay Sullivan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接