15

My app needs to load plugins into separate app domains and then execute some code inside of them asynchronously. I've written some code to wrap Task in marshallable types:

static class RemoteTask
{
    public static async Task<T> ClientComplete<T>(RemoteTask<T> remoteTask,
                                                  CancellationToken cancellationToken)
    {
        T result;

        using (cancellationToken.Register(remoteTask.Cancel))
        {
            RemoteTaskCompletionSource<T> tcs = new RemoteTaskCompletionSource<T>();
            remoteTask.Complete(tcs);
            result = await tcs.Task;
        }

        await Task.Yield(); // HACK!!

        return result;
    }

    public static RemoteTask<T> ServerStart<T>(Func<CancellationToken, Task<T>> func)
    {
        return new RemoteTask<T>(func);
    }
}

class RemoteTask<T> : MarshalByRefObject
{
    readonly CancellationTokenSource cts = new CancellationTokenSource();
    readonly Task<T> task;

    internal RemoteTask(Func<CancellationToken, Task<T>> starter)
    {
        this.task = starter(cts.Token);
    }

    internal void Complete(RemoteTaskCompletionSource<T> tcs)
    {
        task.ContinueWith(t =>
        {
            if (t.IsFaulted)
            {
                tcs.TrySetException(t.Exception);
            }
            else if (t.IsCanceled)
            {
                tcs.TrySetCancelled();
            }
            else
            {
                tcs.TrySetResult(t.Result);
            }
        }, TaskContinuationOptions.ExecuteSynchronously);
    }

    internal void Cancel()
    {
        cts.Cancel();
    }
}

class RemoteTaskCompletionSource<T> : MarshalByRefObject
{
    readonly TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();

    public bool TrySetResult(T result) { return tcs.TrySetResult(result); }
    public bool TrySetCancelled() { return tcs.TrySetCanceled(); }
    public bool TrySetException(Exception ex) { return tcs.TrySetException(ex); }

    public Task<T> Task
    {
        get
        {
            return tcs.Task;
        }
    }
}

It's used like:

sealed class ControllerAppDomain
{
    PluginAppDomain plugin;

    public Task<int> SomethingAsync()
    {
        return RemoteTask.ClientComplete(plugin.SomethingAsync(), CancellationToken.None);
    }
}

sealed class PluginAppDomain : MarshalByRefObject
{
    public RemoteTask<int> SomethingAsync()
    {
        return RemoteTask.ServerStart(async cts =>
        {
            cts.ThrowIfCancellationRequested();
            return 1;
        });
    }
}

But I've run into a snag. If you look in ClientComplete, there's a Task.Yield() I've inserted. If I comment this line, ClientComplete will never return. Any ideas?

Cory Nelson
  • 29,236
  • 5
  • 72
  • 110
  • 2
    Check out search results for "c# async deadlock ConfigureAwait" like http://stackoverflow.com/questions/13489065/best-practice-to-call-configureawait-for-all-server-side-code as I think it would be a solution. – Alexei Levenkov Feb 28 '13 at 18:29
  • I'm not able to repro this. `ControllerAppDomain.SomethingAsync` never hangs for me, whether I block on it or use `await`, whether in a thread pool context or a single-threaded context. Are you sure the code above duplicates the problem? – Stephen Cleary Feb 28 '13 at 18:49
  • @StephenCleary I just tried the code on another machine and can't reproduce it there either. Interesting. – Cory Nelson Feb 28 '13 at 20:09
  • It may be helpful if you include all the callstacks for relevant threads at the time of the hang. – Andrew Arnott Mar 03 '13 at 18:14
  • where is the appdomain created and where would the plugin all be loaded ? It seems like using this method will require plugin developers to inherit from MarshalByRefObject. – Frank Q. Jan 25 '17 at 06:31
  • The code here works great, thanks for the help. One thing i would be interested to know though, is that in the scenario i have, i am making the cross app domain async call from the UI thread and when the call is awaited and returns, i would like to return back to the UI thread. Is that possible? @StephenCleary – user2477533 Feb 01 '19 at 08:59
  • @user2477533: I haven't done much with async cross-AppDomain calls. If you can [get a Task back to your calling code](https://social.msdn.microsoft.com/Forums/vstudio/en-US/28277f25-5f5d-4b7c-bf1f-402937fc9f31/tasks-across-appdomain?forum=parallelextensions), then you can await it with the normal threading semantics. – Stephen Cleary Feb 01 '19 at 10:39

1 Answers1

2

My best guess is that you are facing these issues because of the async method that contains await and this is managed via the ThreadPool which can allocate some recycled Thread.

Reference Best practice to call ConfigureAwait for all server-side code

Actually, just doing an await can do that(put you on a different thread). Once your async method hits an await, the method is blocked but the thread returns to the thread pool. When the method is ready to continue, any thread is snatched from the thread pool and used to resume the method.

Try to streamline the code, generate threads for baseline cases and performance is last.

Community
  • 1
  • 1
Alexandru Lache
  • 477
  • 2
  • 14