Using BeginInvoke/EndInvoke in a multithreaded fashion. How do AsyncCallback, AsyncWaitHandle and IsCompleted interact?
Asked Answered
M

2

4

Andreas Huber's answer to this question gave me an idea to implement Concurrent<T> with async delegates instead of the ThreadPool. However, I am finding it harder to understand what's going on when an AsyncCallback is passed to BeginInvoke, especially when several threads have access to IAsyncResult. Unfortunately, this case doesn't seem to be covered at MSDN or anywhere I could find. Moreover, all articles I could find were either written before closures and generics were available or just seem that way. There are several questions (and the answers which I hope are true, but I am ready to be disappointed):

1) Would using a closure as an AsyncCallback make any difference?
(Hopefully not)
2) If a thread waits on the AsyncWaitHandle, will it be signaled
a) before the callback starts or b) after it finishes?
(Hopefully b)
3) While the callback is running, what will IsCompleted return? Possibilities I can see:
a) true; b) false; c) false before the callback calls EndInvoke, true after.
(Hopefully b or c)
4) Will DisposedObjectException be thrown if some thread waits on the AsyncWaitHandle after EndInvoke is called?
(Hopefully not, but I expect yes).

Provided the answers are as I hope, this seems like it should work:

public class Concurrent<T> { 
    private IAsyncResult _asyncResult;
    private T _result;

    public Concurrent(Func<T> f) { // Assume f doesn't throw exceptions
        _asyncResult = f.BeginInvoke(
                           asyncResult => {
                               // Assume assignment of T is atomic
                               _result = f.EndInvoke(asyncResult); 
                           }, null);
    }

    public T Result {
        get {
            if (!_asyncResult.IsCompleted)
                // Is there a race condition here?
                _asyncResult.AsyncWaitHandle.WaitOne();
            return _result;  // Assume reading of T is atomic
        }
    ...

If the answers to the questions 1-3 are the ones I hope for, there should be no raace condition here, as far as I can see.

Moisesmoishe answered 2/1, 2009 at 14:30 Comment(0)
W
2

Question 1

I think part of the problem is misconception. IAsyncResult is not accessed from multiple threads unless you explicitly pass it to one. If you look at the implementation for mos Begin*** style API's in the BCL, you'll notice the IAsyncResult is only ever created and destroyed from the thread where the Begin*** or End*** call actually occur.

Question 2

AsyncWaitHandle should be signaled after the operation is 100% complete.

Question 3

IsCompleted should return true once the underlying operation is complete (no more work to do). The best way to view IsComplete is that if the value is

  1. true -> Calling End*** will return immediately
  2. false -> Callind End*** will block for some period of time

Question 4

This is implementation dependent. There is no way to really give a blanket answer here.

Samples

If you are interested in an API which allows you to easily run a delegate on another thread and access the result when finished, check out my RantPack Utility Library. It's available in source and binary form. It has a fully fleshed out Future API which allows for the concurrent running of delegates.

Additionally there is an implementation of IAsyncResult which covers most of the questions in this post.

Wallinga answered 2/1, 2009 at 14:46 Comment(5)
While your answers would be very much applicable if I wanted to implement IAsyncResult here, I don't. I am asking about the behaviour of System.Runtime.Remoting.Messaging.AsyncResult, the one actually returned by BeginInvoke.Moisesmoishe
Question 1: This is precisely what I meant by "Unfortunately, this case doesn't seem to be covered at MSDN or anywhere I could find." I am not exposing _asyncResult directly, but only through waiting in the Result property; but the implementations in the framework don't even do that.Moisesmoishe
Question 2: Good, that's what I hoped for.Moisesmoishe
Question 3: What if IsCompleted is true and EndInvoke has been called already? Is calling EndInvoke again safe?Moisesmoishe
Question 4: I understand, and that's why I was asking about a specific implementation.Moisesmoishe
U
2

I've been looking into async calls just recently. I found a pointer to an article with an example implementation of an IAsyncResult by respected author Jeffrey Richter. I learned a lot about how async calls work by studying this implementation.

You might also see if you can download and examine the source code for the System.Runtime.Remoting.Messaging.AsyncResult you're specifically concerned with. Here's a link to instructions on how to do this in Visual Studio.

To add a bit to JaredPar's good answers...

1: I believe if you define a closure which can be assigned to a variable of type AsyncCallback (takes an IAsyncResult and returns void) it should work as you would expect a closure to work as that delegate, but I'm not sure if there could be scope issues. The originating local scope should have returned long before the callback gets invoked (that's what makes it an asynchronous operation), so bear that in mind with respect to references to local (stack) variables and how that will behave. Referencing member variables should be fine, I would think.

2: I think from your comment that you may have misunderstood the answer to this one. In Jeffrey Richter's example implementation the wait handle is signaled before the callback is invoked. If you think about it, it has to be this way. Once it invokes the callback it loses control of the execution. Suppose the callback method throws an exception.... execution could unwind back past the method which invoked the callback and thus prevent it from ever later signaling the wait handle! So the wait handle needs to be signalled before callback is invoked. They're also much closer in time if done in that order than if it signals the wait handle only after the callback has returned.

3: As JaredPar says, IsCompleted should be returning true before the callback and before the wait handle is signaled. This makes sense because if IsCompleted is false you would expect the call to EndInvoke to block, and the whole point of the wait handle (as with the callback) is to know when the result is ready and it won't block. So, first IsCompleted is set to true, then the wait handle is signalled, and then the callback is called. See how Jeffrey Richter's example does it. However, you probably should try to avoid assumptions about the order in which these three methods (polling, wait handle, callback) might detect the completion, because it's possible to implement them in a different order than expected.

4: I can't help you there, except that you might find the answer by debugging into the framework source code for the implementation you're curious about. Or you could probably come up with an experiment to find out... or set up a good experiment and debug into the framework source to be really sure.

Unruly answered 2/9, 2009 at 8:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.