After many scratches on my head, I was finally able to find a solution to my initial question. First of all, let me say that I have many excellent answers and I checked them all (commenting on each of the results). The main problems were that all the proposed solutions led to locks (leading to a 100% timeout scenario) or otherwise made the synchronous asynchronous process different. I donβt like answering my own question (the first time ever), but in this case I took advice on the StackOverflow FAQ, since I really learned my own lesson and wanted to share it with the community.
In the end, I combined the proposed solutions with calling delagates into alternative AppDomains. This is a bit more code, and it's a bit expensive, but it avoids the locks and allows for the completely asynchronous calls that I need. Here is a bit ...
First I needed something to call a delegate in another AppDomain
/// <summary> /// Invokes actions in alternate AppDomains /// </summary> public static class DomainInvoker { /// <summary> /// Invokes the supplied delegate in a new AppDomain and then unloads when it is complete /// </summary> public static T ExecuteInNewDomain<T>(Delegate delegateToInvoke, params object[] args) { AppDomain invocationDomain = AppDomain.CreateDomain("DomainInvoker_" + delegateToInvoke.GetHashCode()); T returnValue = default(T); try { var context = new InvocationContext(delegateToInvoke, args); invocationDomain.DoCallBack(new CrossAppDomainDelegate(context.Invoke)); returnValue = (T)invocationDomain.GetData("InvocationResult_" + invocationDomain.FriendlyName); } finally { AppDomain.Unload(invocationDomain); } return returnValue; } [Serializable] internal sealed class InvocationContext { private Delegate _delegateToInvoke; private object[] _arguments; public InvocationContext(Delegate delegateToInvoke, object[] args) { _delegateToInvoke = delegateToInvoke; _arguments = args; } public void Invoke() { if (_delegateToInvoke != null) AppDomain.CurrentDomain.SetData("InvocationResult_" + AppDomain.CurrentDomain.FriendlyName, _delegateToInvoke.DynamicInvoke(_arguments)); } } }
Second I needed something to organize the collection of the necessary parameters and collect / resolve the results. It will also determine the timeout and workflows that will be invoked asynchronously in the alternate AppDomain.
Note. In my tests, I extended the work dispatcher method to accept random time intervals to notice that everything works as expected in both timeouts and in the absence of a timeout.
public delegate IResponse DispatchMessageWithTimeoutDelegate(IRequest request, int timeout = MessageDispatcher.DefaultTimeoutMs); [Serializable] public sealed class MessageDispatcher { public const int DefaultTimeoutMs = 500;
Third I need something worthwhile for the real thing, which asynchronously launches mailing lists
Note. This is just to demonstrate the asynchronous behavior I need. In fact, those First and Second elements demonstrate isolation of timeout behavior on alternative threads. This simply demonstrates how these resources are used.
public delegate IResponse DispatchMessageDelegate(IRequest request); class Program { static int _responsesReceived; static void Main() { const int max = 500; for (int i = 0; i < max; i++) { SendRequest(new Request()); } while (_responsesReceived < max) { Thread.Sleep(5); } } static void SendRequest(IRequest request, int timeout = MessageDispatcher.DefaultTimeoutMs) { var dispatcher = new DispatchMessageWithTimeoutDelegate(SendRequestWithTimeout); dispatcher.BeginInvoke(request, timeout, SendMessageCallback, request); } static IResponse SendRequestWithTimeout(IRequest request, int timeout = MessageDispatcher.DefaultTimeoutMs) { var dispatcher = new MessageDispatcher(); return dispatcher.SendRequest(request, timeout); } static void SendMessageCallback(IAsyncResult ar) { var result = (AsyncResult)ar; var caller = (DispatchMessageWithTimeoutDelegate)result.AsyncDelegate; Response response; try { response = (Response)caller.EndInvoke(ar); } catch (Exception) { response = null; } Interlocked.Increment(ref _responsesReceived); } }
In retrospect, this approach has some unintended consequences. Since the working method is found in an alternative AppDomain, this adds additional protection for exceptions (although this can also hide them), allows you to load and unload other managed assemblies (if required), and allows you to define highly restricted or specialized security contexts. This requires a bit more performance but provided a framework for answering my original question. Hope this helps someone.