AsyncResult, Calback and WaitHandles

D

deja

I have a web service that reads data from the database in a number of
asynchronous calls.

I have a callback for each call so that I can call EndInvoke so that I
can trap any errors.

I also have a WaitHandle.WaitAll.

The trouble is that the WaitAll completes before the callbacks are all
complete. So the web service returns results despite the fact that one
of the database calls errored.

How do I trap the error and bubble it up to the original web service
thread?

Thanks
Phil
 
B

Bob Altman

If I understand this correctly, you use the WaitAll to block the main thread
until the asynchronous calls are done. But it unblocks before the async
callbacks have a chance to execute.

Under the covers, each async process signals its associated Event object
prior to calling its callback routine. Thus the fatal flaw in your existing
design: The Event objects are (by design) signalled before the callbacks
are delivered.

I would use a SyncLock (lock in C#) block to arbitrate access to a private
variable that keeps track of the progress of asynchronous work. When the
final async callback completes, you can use whatever private mechanism you
like best to wake up or queue a call onto the main thread.

The main point here is that you need to implement your own private
synchronization mechanism. When dealing with async code, don't try to use
both the WaitHandle and the Callback. It's an either/or proposition.

- Bob
 
D

deja

thanks for this Bob. Am starting to understand now. So if the main
thread needs results from the async process then a callback is not
going to be any good to me if that main thread is a web service method
because by the time the callback comes the main thread might have
gone?

So I will use Wait and then do the EndInvoke after that. I need to
figure out how WaitAny works because I've tried that but am not quite
sure how you determine which waithandle to process.... I could use
WaitAll but WaitAny seems more efficient.

Also, just to check regarding the callback, if I was using, for
example a Windows app, and wanted to, as you put it, wake up the main
thread or queue a call, how would I go about doing that> Do you have
any sample code that shows how the worker thread communicates back to
the main thread?

thanks a lot for this.
 
B

Bob Altman

I have only a passing familiarity with Web Services, but I don't see why
that should stop me from offering advice. ;-)

If your web service wants to kick off some asynchronous processing, then it
seems to me that you have several options:

1. You can block the web service thread until the async operations have
completed. You would want to do this if, for example, it takes less time to
run your async operations in parallel than it would to serialize them (call
them them one after another synchronously). In this case, your
initialization code would be:

private m_myEvent As New Threading.AutoResetEvent(False)

Your web service code would be:

StartAsyncStuff()
m_myEvent.WaitOne()
ProcessAsyncOperationResults

Your async callback routine would be:

Try
EndXxx()
Catch WhateverException
Store exception info in a global variable
End Try

' Wake up the main thread
If (all async work is done) Then m_myEvent..Set

2. You can start the async processing and immediately return control back to
the web method caller. In this case, the caller would be clueless about the
results of the async processing. You could use a local logging mechanism
(such as the event log) to record any problems that arise from your async
processing.

3. You can combine options 1 and 2 by having your routine (let's call it
BeginXxx) allocate a state object in a hash table or dictionary with a
unique index. BeginXxx would start the async processing, passing the state
object to each async routine's "state" argument, then return the unique
index to the web method caller. As the async routines complete, BeginXxx
would fetch the state object from the IAsyncResult.State member, and store
the results in the state object. You would then provide a companion routine
(let's call it EndXxx) which takes as an argument the index returned by
BeginXxx. EndXxx fetches the state object from the dictionary and blocks
(as shown above) until all of the async processing is done.

On your last question (how to queue a call to the main thread): If your app
is a Windows Forms app, then all of the event handlers that are called in
your "code behind the form" are called on the "main UI" thread. Under the
covers, this is actually the thread that spends most of its time waiting for
the Windows message pump to deliver Windows messages to it. If, for
example, you call someStream.BeginWrite() in a button's Click event then the
completion callback will happen on a thread pool thread, not on the main UI
thread. If you try to access the main form or any of its controls in that
callback then you will either get unpredictable results (it may work or you
may get some really strange error) in VS 2003, or you will get an exception
in VS 2005. The exception to the "don't access the main form or its
controls" rule is InvokeRequired, Invoke, and BeginInvoke. I use this
design pattern to transfer control from a routine called on a non-UI thread
back to the UI thread:

Sub SomeRoutine(SomeArgs)
If Me.InvokeRequired() Then
' Queue the call to the UI thread
Dim deleg As New SomeRoutineDeleg(AddressOf SomeRoutine)
Dim args() as Object = {SomeArgs}
Me.BeginInvoke(deleg, args)
Else
' We're on the UI thread...
do the work
End If
End Sub

Good luck...

- Bob
 
D

deja

I have only a passing familiarity with Web Services, but I don't see why
that should stop me from offering advice. ;-)

If your web service wants to kick off some asynchronous processing, then it
seems to me that you have several options:

1. You can block the web service thread until the async operations have
completed. You would want to do this if, for example, it takes less time to
run your async operations in parallel than it would to serialize them (call
them them one after another synchronously). In this case, your
initialization code would be:

private m_myEvent As New Threading.AutoResetEvent(False)

Your web service code would be:

StartAsyncStuff()
m_myEvent.WaitOne()
ProcessAsyncOperationResults

Your async callback routine would be:

Try
EndXxx()
Catch WhateverException
Store exception info in a global variable
End Try

' Wake up the main thread
If (all async work is done) Then m_myEvent..Set

2. You can start the async processing and immediately return control back to
the web method caller. In this case, the caller would be clueless about the
results of the async processing. You could use a local logging mechanism
(such as the event log) to record any problems that arise from your async
processing.

3. You can combine options 1 and 2 by having your routine (let's call it
BeginXxx) allocate a state object in a hash table or dictionary with a
unique index. BeginXxx would start the async processing, passing the state
object to each async routine's "state" argument, then return the unique
index to the web method caller. As the async routines complete, BeginXxx
would fetch the state object from the IAsyncResult.State member, and store
the results in the state object. You would then provide a companion routine
(let's call it EndXxx) which takes as an argument the index returned by
BeginXxx. EndXxx fetches the state object from the dictionary and blocks
(as shown above) until all of the async processing is done.

On your last question (how to queue a call to the main thread): If your app
is a Windows Forms app, then all of the event handlers that are called in
your "code behind the form" are called on the "main UI" thread. Under the
covers, this is actually the thread that spends most of its time waiting for
the Windows message pump to deliver Windows messages to it. If, for
example, you call someStream.BeginWrite() in a button's Click event then the
completion callback will happen on a thread pool thread, not on the main UI
thread. If you try to access the main form or any of its controls in that
callback then you will either get unpredictable results (it may work or you
may get some really strange error) in VS 2003, or you will get an exception
in VS 2005. The exception to the "don't access the main form or its
controls" rule is InvokeRequired, Invoke, and BeginInvoke. I use this
design pattern to transfer control from a routine called on a non-UI thread
back to the UI thread:

Sub SomeRoutine(SomeArgs)
If Me.InvokeRequired() Then
' Queue the call to the UI thread
Dim deleg As New SomeRoutineDeleg(AddressOf SomeRoutine)
Dim args() as Object = {SomeArgs}
Me.BeginInvoke(deleg, args)
Else
' We're on the UI thread...
do the work
End If
End Sub

Good luck...

- Bob


thanks for this. There's something I obviously haven't quite got
sussed. To do with WaitAny. If I use SqlCommand.BeginExecuteReader
then I can use WaitAny and use the index returned to process the
relevant results.

If I use a delegate to one of my own functions, use BeginInvoke and
then use WaitAny, I keep getting 0 as the WaitHandle index which then
causes an error because you can only call EndInvoke once.

Do I have to use this AutoResetEvent within the function called? How
do I do this? - my function is currently called like this:

WaitHandle[] wh = new WaitHandle[2];
accDelegate accDel = new accDelegate(new Account().GetDetails);
IAsyncResult res1 = accDel.BeginInvoke(123);

ordDelegate ordDel = new ordDelegate(new Order().GetDetails);
IAsyncResult res2 = ordDel.BeginInvoke(123);

wh[0] = res1.AsyncWaitHandle;
wh[1] = res2.AsyncWaitHandle;

Hashtable accounts;
Hashtable orders;

for (int i=0;i<2;i++)
{
int index = WaitHandle.WaitAny(wh);
if (index == WaitHandle.WaitTimeout)
throw new Exception("Timeout.");

switch(index)
{
case 0:
accounts = accDel.EndInvoke(res1);
break;
case 1:
orders = ordDel.EndInvoke(res2);
break;
}

}

carry on........


But I get index returned as 0 twice in a row......
 
B

Bob Altman

thanks for this. There's something I obviously haven't quite got
sussed. To do with WaitAny. If I use SqlCommand.BeginExecuteReader
then I can use WaitAny and use the index returned to process the
relevant results.

If I use a delegate to one of my own functions, use BeginInvoke and
then use WaitAny, I keep getting 0 as the WaitHandle index which then
causes an error because you can only call EndInvoke once.

Do I have to use this AutoResetEvent within the function called? How
do I do this? - my function is currently called like this:

WaitHandle[] wh = new WaitHandle[2];
accDelegate accDel = new accDelegate(new Account().GetDetails);
IAsyncResult res1 = accDel.BeginInvoke(123);

ordDelegate ordDel = new ordDelegate(new Order().GetDetails);
IAsyncResult res2 = ordDel.BeginInvoke(123);

wh[0] = res1.AsyncWaitHandle;
wh[1] = res2.AsyncWaitHandle;

Hashtable accounts;
Hashtable orders;

for (int i=0;i<2;i++)
{
int index = WaitHandle.WaitAny(wh);
if (index == WaitHandle.WaitTimeout)
throw new Exception("Timeout.");

switch(index)
{
case 0:
accounts = accDel.EndInvoke(res1);
break;
case 1:
orders = ordDel.EndInvoke(res2);
break;
}

}

carry on........


But I get index returned as 0 twice in a row......

That's curious. It's behaving as if the first WaitHandle is still signalled
the second time around. I would have expected that the delegate object
would return a reference to an AutoResetEvent object, which would reset its
"signalled" state when your WaitAny() call completes. (BTW, you left off
the timeout parameter in the WaitAny call, so you will never see a
"TimedOut" result.)

Take a look at the Type of the object returned by res1.AsyncWaitHandle.

Your other option is to simply call the two EndInvoke routines in lieu of
the entire "for" loop:

res1 = accDel.BeginInvoke;
res2 = ordDel.VeginInvoke;

accDel.EndInvoke(res1);
ordDel.EndInvoke(res2);

This code blocks until both async routines complete, which is the same
behavior that your example code exhibits.
 
D

deja

thanks for this. There's something I obviously haven't quite got
sussed. To do withWaitAny. If I use SqlCommand.BeginExecuteReader
then I can useWaitAnyand use the index returned to process the
relevant results.
If I use a delegate to one of my own functions, use BeginInvoke and
then useWaitAny, I keep getting 0 as the WaitHandle index which then
causes an error because you can only call EndInvokeonce.
Do I have to use this AutoResetEvent within the function called? How
do I do this? - my function is currently called like this:
WaitHandle[] wh = new WaitHandle[2];
accDelegate accDel = new accDelegate(new Account().GetDetails);
IAsyncResult res1 = accDel.BeginInvoke(123);
ordDelegate ordDel = new ordDelegate(new Order().GetDetails);
IAsyncResult res2 = ordDel.BeginInvoke(123);
wh[0] = res1.AsyncWaitHandle;
wh[1] = res2.AsyncWaitHandle;
Hashtable accounts;
Hashtable orders;
for (int i=0;i<2;i++)
{
int index = WaitHandle.WaitAny(wh);
if (index == WaitHandle.WaitTimeout)
throw new Exception("Timeout.");
switch(index)
{
case 0:
accounts = accDel.EndInvoke(res1);
break;
case 1:
orders = ordDel.EndInvoke(res2);
break;
}

carry on........
But I get index returned as 0 twice in a row......

That's curious. It's behaving as if the first WaitHandle is still signalled
the second time around. I would have expected that the delegate object
would return a reference to an AutoResetEvent object, which would reset its
"signalled" state when yourWaitAny() call completes. (BTW, you left off
the timeout parameter in theWaitAnycall, so you will never see a
"TimedOut" result.)

Take a look at the Type of the object returned by res1.AsyncWaitHandle.

Your other option is to simply call the two EndInvoke routines in lieu of
the entire "for" loop:

res1 = accDel.BeginInvoke;
res2 = ordDel.VeginInvoke;

accDel.EndInvoke(res1);
ordDel.EndInvoke(res2);

This code blocks until both async routines complete, which is the same
behavior that your example code exhibits.

Aha, I didn't see your reply until now but today I figured out the
problem anyway. It returns a ManualResetEvent object - don't know why,
don't know what determines it - but anyway, when I cast the WaitHandle
to a ManualResetEvent and use the Reset method after getting the
WaitAny , it all works fine. I write this because after searching for
the solution in newsgroups I never found the answer - so maybe this
will help someone in the future.

thanks
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top