Deadlock when combining app domain remoting and tasks
Asked Answered
D

1

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?

Duly answered 28/2, 2013 at 18:5 Comment(7)
Check out search results for "c# async deadlock ConfigureAwait" like #13489565 as I think it would be a solution.Pasticcio
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?Blindly
@StephenCleary I just tried the code on another machine and can't reproduce it there either. Interesting.Duly
It may be helpful if you include all the callstacks for relevant threads at the time of the hang.Rom
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.Playful
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? @StephenClearyMight
@user2477533: I haven't done much with async cross-AppDomain calls. If you can get a Task back to your calling code, then you can await it with the normal threading semantics.Blindly
B
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.

Bar answered 27/5, 2013 at 12:27 Comment(1)
Whoever down voted this answer, it would be helpful to leave a comment explaining why.Luedtke

© 2022 - 2024 — McMap. All rights reserved.