Search code examples
c#.netconcurrencyappdomainabort

How do I pass CancellationToken across AppDomain boundary?


I have a command object, doing work based on a request from a request queue. This particular command will execute its work in a child appdomain. Part of doing its work in the child appdomain involves blocking on a ConcurrentQueue operation (eg, Add or Take). I need to be able to propagate an abort signal through the request queue, across to the child appdomain, and to wake up the worker threads therein.

Therefore, I think I need to pass a CancellationToken across the AppDomain boundary.

I tried creating a class which inherits from MarshalByRefObject:

protected class InterAppDomainAbort : MarshalByRefObject, IAbortControl
    {
        public InterAppDomainAbort(CancellationToken t)
        {
            Token = t;
        }

        [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
        public override object InitializeLifetimeService()
        {
            return null;
        }

        public CancellationToken Token
        {
            get;
            private set;
        }

    };

and passing this as an argument on the worker function:

// cts is an instance variable which can be triggered by another thread in parent appdomain
cts = new CancellationTokenSource();
InterAppDomainAbort abortFlag = new InterAppDomainAbort(cts.Token);
objectInRemoteAppDomain = childDomain.CreateInstanceAndUnwrap(...);

// this call will block for a long while the work is being performed.
objectInRemoteAppDomain.DoWork(abortFlag);

But I still get an exception when the objectInRemoteAppDomain tries to access the Token getter property:

System.Runtime.Serialization.SerializationException: Type 'System.Threading.CancellationToken' in Assembly 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' is not marked as serializable.

My question is: How can I propagate the abort/cancellation signal across the appdomains and wake up threads that may be blocked in .NET concurrency data structures (where CancellationToken arguments are supported).


Solution

  • It's been a while since I looked at any cross-AppDomain stuff, so there might be problems with this code that I haven't realised, but it seems to do the job. The fundamental problem is that there seems no way to transfer a CancellationToken[Source] from one AppDomain to another. So I create two sources, with the primary set up to cancel the secondary when appropriate.

    The fact that there are two separate token sources in this scenario could of course be a problem, but I don't think you're getting around the fact that lack of serialisability prevents you from using the same one in two separate AppDomains anyway.

    Standard caveats about minimal error-checking, Dispose implementations, etc.

    // I split this into a separate interface simply to make the boundary between
    // canceller and cancellee explicit, similar to CancellationTokenSource itself.
    public interface ITokenSource
    {
        CancellationToken Token { get; }
    }
    
    public class InterAppDomainCancellable: MarshalByRefObject,
                                            ITokenSource,
                                            IDisposable
    {
        public InterAppDomainCancellable()
        {
            cts = new CancellationTokenSource();
        }
    
        public void Cancel() { cts.Cancel(); }
    
        // Explicitly implemented to make it less tempting to call Token
        // from the wrong side of the boundary.
        CancellationToken ITokenSource.Token { get { return cts.Token; } }
    
        public void Dispose() { cts.Dispose(); }
    
        private readonly CancellationTokenSource cts;
    }
    
    // ...
    
    // Crucial difference here is that the remotable cancellation source
    // also lives in the other domain.
    interAppDomainCancellable = childDomain.CreateInstanceAndUnwrap(...);
    
    var primaryCts = new CancellationTokenSource();
    // Cancel the secondary when the primary is cancelled.
    // CancellationToken.Register returns a disposable object which unregisters when disposed.
    using (primaryCts.Token.Register(() => interAppDomainCancellable.Cancel()))
    {
        objectInRemoteAppDomain = childDomain.CreateInstanceAndUnwrap(...);
        // DoWork expects an instance of ITokenSource.
        // It can access Token because they're all in the same domain together.
        objectInRemoteAppDomain.DoWork(interAppDomainCancellable);
        // ... some other work which might cancel the primary token.
    }