The only given explanation was:
In a related post, the blog’s author stated that
why does BeginInvoke break when all the TP [Transparent Proxy]and RP [Real Proxy] plumbing is in place? This question is much trickier than it seems to be, after a bit of research on the default implementation of TP and RP mechanism used by .NET remoting using both .NET reflector and the rotor 2.0 implementation of CLR, I finally figure out that for the current implementation of TP and RP mechanism, it only supports asynchronous call when the default RemotingProxy is in place, since the ServiceChannelProxy is WCF’s own implementation, it gets ignored by the BeginInvoke mechanism, and the BeginInvoke call against WCF’s proxies will be performed synchronously.
This explanation was not completely clear to me, however it provided some clues to the reasons why the asynchronous call could block.
So, my next steps were to grab the SSCLI source code and analyze how BeginInvoke is implemented. My observations follow:
- When a compiler defines a delegate class, the BeginInvoke method does not contain any implementation. Instead, this method is annotated with the MethodImplAttribute attribute, with the MethodCodeType field set to MethodCodeType.Runtime. This means that the BeginInvoke implementation will be given by the runtime.
- In SSCLI, the class COMDelegate (defined in the comdelegate.h and comdelegate.cpp files) represents the native methods of the Delegate class. In this C++ class, the GetInvokeMethodStub method is used to get the stubs for each of the synchronous and asynchronous invoke methods. For the BeginInvoke method, this stub is returned by the TheAsyncDelegateStub function (defined in remoting.h).
- The stub returned by the TheAsyncDelegateStub method is created by the CTPMethodTable::EmitSetupFrameCode method (defined in remotingx86.h). This last method emits x86 assembly code that includes a call to the CTPMethodTable::OnCall method (defined in remoting.cpp).
- The CTPMethodTable::OnCall, among other things, tests if the target of the delegate is a transparent proxy.
- If it IS NOT a transparent proxy:
- the OnCall method calls the RemotingProxy.Invoke static method, which performs the expected ThreadPool.QueueUserWorkItem call.
- However, if it IS a transparent proxy:
- the OnCall method obtains the real proxy behind the transparent proxy, and calls the RealProxy.PrivateInvoke method.
- This method ends up calling the virtual Invoke method of ServiceChannelFactory (the type of real proxy behind the transparent proxy returned by a ChannelFactory<T>). This method checks if the called channels’ method is synchronous, and if so completes the call synchronously, despite the call being made via a delegate’s BeginInvoke.
So, in conclusion:
- A delegates’s BeginInvoke method call is handled by the remoting infrastructure. Aparently, this so to optimize the asynchronous remoting calls, avoiding extra context switchs (see this post by Chris Brumme).
- If the target of the delegate is not a transparent proxy, then a regular ThreadPool.QueueUserWorkItem is performed.
- If the target is a transparent proxy, then the BeginInvoke call is processed by the real proxy. If the delegate’s method is synchronous, then the WCF’s real proxy performs a synchronous service call.
Note, however, that an asynchronous delegate call on the synchronous method of the “client proxy” generated by svcutil.exe will not block waiting for the service’s response. This is so because this “client proxy” is a regular class and not a transparent or real proxy, so the “normal” BeginInvoke behavior applies.
This does not mean that using asynchronous delegate calls on the synchronous “client proxy” methods is recommended. The appropriate method is to use the /async switch of the svcutil.exe tool to generate a service interface and a “client proxy” with asynchronous methods, as described in the original Nicholas Allen’s post.