locked
Tasks across AppDomain RRS feed

  • Question

  • What is the best way to use Tasks across AppDomain boundaries? I have an API with async members like this:

    Task<TResult> DoSomething(string[] args);
    

    This works well within the AppDomain, but Task cannot be serialized across.

    Is there a best practice pattern for cross-domain async calls that allow one to wait for completion and to obtain the task result?

    Friday, July 23, 2010 6:28 PM

Answers

  • As you say, tasks can't be serialized across an AppDomain boundary.  However, you can do your own marshaling to make this possible.

    TPL provides the TaskCompletionSource<T> class, which gives you as the owner of the TCS<T> the ability to control the lifecycle of its task, e.g.

    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    Task<int> t = tcs.Task;
    ... // later on
    tcs.SetResult(42); // will complete t with its Result == 42

    In this manner, your DoSomething(string[]args) can hand back a Task<TResult> from a TCS that you create inside your DoSomething method.  Your method can then launch the asynchronous work across AppDomain boundaries in whatever manner is most appropriate, and you can marshal the results back from that other AppDomain.  Once you do that, you can then set the results onto that task you handed back from your method.  The caller of your DoSomething method will get a task like any other, and you can handle all of the relevant details inside of your method.

    Here's an example of the kind of thing I have in mind.

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    class Program
    {
      static void Main()
      {
        var ad = AppDomain.CreateDomain("WhereTheWorkHappens");
    
        Task<string> t = DoWorkInOtherDomain(ad);
        Console.WriteLine("waiting...");
        Console.WriteLine(t.Result);
    
        Console.ReadLine();
      }
    
      static Task<string> DoWorkInOtherDomain(AppDomain ad)
      {
        var ch = new MarshaledResultSetter<string>();
    
        Worker worker = (Worker)ad.CreateInstanceAndUnwrap(typeof(Worker).Assembly.FullName, typeof(Worker).FullName);
        worker.DoWork(ch);
    
        return ch.Task;
      }
    
      class Worker : MarshalByRefObject
      {
        public void DoWork(MarshaledResultSetter<string> callback)
        {
          ThreadPool.QueueUserWorkItem(delegate
          {
            Thread.SpinWait(500000000);
            callback.SetResult(AppDomain.CurrentDomain.FriendlyName);
          });
        }
      }
    
      class MarshaledResultSetter<T> : MarshalByRefObject
      {
        private TaskCompletionSource<T> m_tcs = new TaskCompletionSource<T>();
        public void SetResult(T result) { m_tcs.SetResult(result); }
        public Task<T> Task { get { return m_tcs.Task; } }
      }
    }

    I hope this helps.

    Saturday, July 24, 2010 1:12 AM
    Moderator
  • Following on to my previous response, here's an example of how you could wrap this up into a cross-AppDomain task marshaler:

    public static class CrossDomainTaskMarshaler
    {
      public static Task<T> Marshal<T>(AppDomain appDomain, Func<Task<T>> function)
      {
        var m = new MarshalableCompletionSource<T>();
        var t = typeof(RemoteWorker<T>);
        var w = (RemoteWorker<T>)appDomain.CreateInstanceAndUnwrap(t.Assembly.FullName, t.FullName);
        w.Run(function, m);
        return m.Task;
      }
    
      private class RemoteWorker<T> : MarshalByRefObject
      {
        public void Run(Func<Task<T>> function, MarshalableCompletionSource<T> marshaler)
        {
          function().ContinueWith(t =>
          {
            if (t.IsFaulted) marshaler.SetException(t.Exception.InnerExceptions.ToArray());
            else if (t.IsCanceled) marshaler.SetCanceled();
            else marshaler.SetResult(t.Result);
          });
        }
      }
    
      private class MarshalableCompletionSource<T> : MarshalByRefObject
      {
        private readonly TaskCompletionSource<T> m_tcs = new TaskCompletionSource<T>();
    
        public void SetResult(T result) { m_tcs.SetResult(result); }
        public void SetException(Exception [] exception) { m_tcs.SetException(exception); }
        public void SetCanceled() { m_tcs.SetCanceled(); }
    
        public Task<T> Task { get { return m_tcs.Task; } }
      }
    }
    

    I could then use to implement a method that needs to launch work in another domain as a task and marshal it back, e.g.

    Task<string> DoWorkInOtherDomain(AppDomain ad)
    {
      return CrossDomainTaskMarshaler.Marshal(ad, () =>
      {
        return Task.Factory.StartNew(() =>
        {
          Thread.Sleep(1000);
          return AppDomain.CurrentDomain.FriendlyName;
        });
      });
    }
    

     

     

     

     

     

    Saturday, July 24, 2010 1:59 AM
    Moderator

All replies

  • As you say, tasks can't be serialized across an AppDomain boundary.  However, you can do your own marshaling to make this possible.

    TPL provides the TaskCompletionSource<T> class, which gives you as the owner of the TCS<T> the ability to control the lifecycle of its task, e.g.

    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    Task<int> t = tcs.Task;
    ... // later on
    tcs.SetResult(42); // will complete t with its Result == 42

    In this manner, your DoSomething(string[]args) can hand back a Task<TResult> from a TCS that you create inside your DoSomething method.  Your method can then launch the asynchronous work across AppDomain boundaries in whatever manner is most appropriate, and you can marshal the results back from that other AppDomain.  Once you do that, you can then set the results onto that task you handed back from your method.  The caller of your DoSomething method will get a task like any other, and you can handle all of the relevant details inside of your method.

    Here's an example of the kind of thing I have in mind.

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    class Program
    {
      static void Main()
      {
        var ad = AppDomain.CreateDomain("WhereTheWorkHappens");
    
        Task<string> t = DoWorkInOtherDomain(ad);
        Console.WriteLine("waiting...");
        Console.WriteLine(t.Result);
    
        Console.ReadLine();
      }
    
      static Task<string> DoWorkInOtherDomain(AppDomain ad)
      {
        var ch = new MarshaledResultSetter<string>();
    
        Worker worker = (Worker)ad.CreateInstanceAndUnwrap(typeof(Worker).Assembly.FullName, typeof(Worker).FullName);
        worker.DoWork(ch);
    
        return ch.Task;
      }
    
      class Worker : MarshalByRefObject
      {
        public void DoWork(MarshaledResultSetter<string> callback)
        {
          ThreadPool.QueueUserWorkItem(delegate
          {
            Thread.SpinWait(500000000);
            callback.SetResult(AppDomain.CurrentDomain.FriendlyName);
          });
        }
      }
    
      class MarshaledResultSetter<T> : MarshalByRefObject
      {
        private TaskCompletionSource<T> m_tcs = new TaskCompletionSource<T>();
        public void SetResult(T result) { m_tcs.SetResult(result); }
        public Task<T> Task { get { return m_tcs.Task; } }
      }
    }

    I hope this helps.

    Saturday, July 24, 2010 1:12 AM
    Moderator
  • Following on to my previous response, here's an example of how you could wrap this up into a cross-AppDomain task marshaler:

    public static class CrossDomainTaskMarshaler
    {
      public static Task<T> Marshal<T>(AppDomain appDomain, Func<Task<T>> function)
      {
        var m = new MarshalableCompletionSource<T>();
        var t = typeof(RemoteWorker<T>);
        var w = (RemoteWorker<T>)appDomain.CreateInstanceAndUnwrap(t.Assembly.FullName, t.FullName);
        w.Run(function, m);
        return m.Task;
      }
    
      private class RemoteWorker<T> : MarshalByRefObject
      {
        public void Run(Func<Task<T>> function, MarshalableCompletionSource<T> marshaler)
        {
          function().ContinueWith(t =>
          {
            if (t.IsFaulted) marshaler.SetException(t.Exception.InnerExceptions.ToArray());
            else if (t.IsCanceled) marshaler.SetCanceled();
            else marshaler.SetResult(t.Result);
          });
        }
      }
    
      private class MarshalableCompletionSource<T> : MarshalByRefObject
      {
        private readonly TaskCompletionSource<T> m_tcs = new TaskCompletionSource<T>();
    
        public void SetResult(T result) { m_tcs.SetResult(result); }
        public void SetException(Exception [] exception) { m_tcs.SetException(exception); }
        public void SetCanceled() { m_tcs.SetCanceled(); }
    
        public Task<T> Task { get { return m_tcs.Task; } }
      }
    }
    

    I could then use to implement a method that needs to launch work in another domain as a task and marshal it back, e.g.

    Task<string> DoWorkInOtherDomain(AppDomain ad)
    {
      return CrossDomainTaskMarshaler.Marshal(ad, () =>
      {
        return Task.Factory.StartNew(() =>
        {
          Thread.Sleep(1000);
          return AppDomain.CurrentDomain.FriendlyName;
        });
      });
    }
    

     

     

     

     

     

    Saturday, July 24, 2010 1:59 AM
    Moderator
  • Thank you very much for your detailed reply. I had not looked at TaskCompletionSource yet.

    This should work, even though my situation is a little different - the Task creation is part of an API  located in the "other" app domain - where the object implementing the API is MarshalByRef.

     

    Tuesday, July 27, 2010 5:24 AM
  • Here is the solution for Cross app domain tasks..

    http://www.pixytech.com/rajnish/2014/09/cross-appdomain-tasks/

    From Client (controlling app domain) you send a request to remote app domain which creates a task and returns RemoteTask ro RemoteTask.

    On client then you wait till task is finished and you can retrieve the results and other status.

    Rajnish Noonia

    Wednesday, September 17, 2014 3:45 PM