以下是代码:
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);
}
}
以下是测试用例:
[TestMethod]
public void TestBeginInvoke()
{
var longOp = new LongOp();
var result = longOp.CallLongOp();
//This can fail
Assert.IsNotNull(result);
}
如果运行此测试用例可能会失败。为什么这么干净?
关于delegate.BeginInvoke如何工作的文档很少。有没有人有他们想分享的见解?
更新 这是一种微妙的竞争条件,在MSDN或其他地方没有详细记录。如接受的答案中所解释的那样,问题是当操作完成时,发出等待句柄的信号,然后执行回调。该信号释放等待的主线程,现在回调执行进入“竞争”。 Jeffry Richter's suggested implementation显示了幕后发生的事情:
// 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);
有关解决方案,请参阅Ben Voigt的回答。该实现不会产生第二个等待句柄的额外开销。
答案 0 :(得分:10)
异步操作完成时,将发出ASyncWaitHandle.WaitOne()信号。同时调用CallBack()。
这意味着WaitOne()之后的代码在主线程中运行,而CallBack在另一个线程中运行(可能与运行DoLongOp()的线程相同)。这会导致竞争条件,其中longOpResult的值在返回时基本上是未知的。
人们可以预料,当CallBack结束时,ASyncWaitHandle.WaitOne()会发出信号,但这不是它的工作方式; - )
您需要另一个ManualResetEvent让主线程等待CallBack设置longOpResult。
答案 1 :(得分:5)
正如其他人所说,result.WaitOne
只意味着BeginInvoke
的目标已经完成,而不是回调。所以只需将后处理代码放入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;
答案 2 :(得分:3)
发生了什么
由于您的操作DoLongOp
已完成,控制将在CallLongOp
内恢复,并且该功能在回调操作完成之前完成。 Assert.IsNotNull(result);
然后在longOpResult = "Completed";
之前执行。
<强>为什么呢? AsyncWaitHandle.WaitOne()只会等待您的异步操作完成,而不是等待回拨
BeginInvoke的回调参数实际上是AsyncCallback delegate,这意味着您的回调是异步调用的。这是设计使然,因为目的是异步处理操作结果(并且是此回调参数的全部目的)。
由于BeginInvoke函数实际上调用了Callback函数,因此IAsyncResult.WaitOne调用仅用于操作,不会影响回调。
请参阅Microsoft documentation(部分执行异步调用完成时的回调方法)。还有一个很好的解释和例子。
如果启动异步调用的线程不需要是处理结果的线程,则可以在调用完成时执行回调方法。回调方法在ThreadPool线程上执行。
<强>解决方案强>
如果要等待操作和回调,则需要自己处理信令。 ManualReset是实现它的一种方式,它确实为您提供了最大的控制权(这也是Microsoft在其文档中完成它的方式)。
以下是使用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();
}
}
对于您给出的示例,最好不要使用回调,而是在CallLongOp函数中处理结果,在这种情况下,操作委托上的WaitOne将正常工作。
答案 3 :(得分:0)
在CallLongOp方法之后执行回调。由于您只在回调中设置变量值,因此它可以为null。 阅读:link text
答案 4 :(得分:0)
我最近遇到了同样的问题,我想出了另一种方法来解决它,它适用于我的情况。但是,如果超时不对您造成影响,请在等待句柄超时时重新检查标志IsCompleted。在我的情况下,等待句柄在阻塞线程之前发出信号,并且在if条件之后立即发出信号,因此在超时后重新检查它将会发挥作用。
while (!AsyncResult.IsCompleted)
{
if (AsyncWaitHandle.WaitOne(10000))
break;
}