ContextBoundObject Throws a Remoting Error After Await

ⅰ亾dé卋堺 提交于 2019-11-29 11:41:58
Matt Smith

Here is a more general workaround.

It has the following deficiencies:

  • It does not support changing the SynchronizationContext within the ContextBoundObject. It will throw in that case.
  • It does not support the case of using await when SynchronizationContext.Current is null and the TaskScheduler.Current is not the TaskScheduler.Default. In this scenario, normally await would capture the TaskScheduler and use that to post the remainder of the work, but since this solution sets the SynchronizationContext the TaskScheduler would not be captured. Thus, when this situation is detected, it will throw.
  • It does not support using .ConfigureAwait(false) since that will cause the SynchronizationContext to not be captured. Unfortunately, I could not detect this case. However, if the user does want to get .ConfigureAwait(false) like behavior for the underlying pass-through SynchronizationContext, they can use a custom awaiter (see https://stackoverflow.com/a/22417031/495262).

One interesting thing here is that I've attempted to create a "pass through" SynchronizationContext. That is, I didn't want to overwrite any existing SynchronizationContext, but rather retain its behavior and layer on top of it the behavior of doing the work in the proper context. Any comments on a better approach are welcome.

    using System;
    using System.Runtime.Remoting.Activation;
    using System.Runtime.Remoting.Contexts;
    using System.Runtime.Remoting.Messaging;
    using System.Threading;
    using System.Threading.Tasks;

    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                var c1 = new Class1();
                var t = c1.Method1();
                Func<Task> f = c1.Method1;
                f.BeginInvoke(null, null);

                Console.ReadKey();
            }
        }

        [MyContext]
        public class Class1 : ContextBoundObject
        {
            private string one = "1";
            public async Task Method1()
            {
                Console.WriteLine(one);
                await Task.Delay(50);
                Console.WriteLine(one);
            }
        }

        sealed class MyContextAttribute : ContextAttribute
        {
            public MyContextAttribute()
                : base("My")
            {
            }

            public override void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
            {
                if (ctorMsg == null)
                    throw new ArgumentNullException("ctorMsg");

                ctorMsg.ContextProperties.Add(new ContributeInstallContextSynchronizationContextMessageSink());
            }

            public override bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg)
            {
                return false;
            }
        }

        sealed class ContributeInstallContextSynchronizationContextMessageSink : IContextProperty, IContributeServerContextSink
        {
            public ContributeInstallContextSynchronizationContextMessageSink()
            {
            }

            public IMessageSink GetServerContextSink(IMessageSink nextSink)
            {
                return new InstallContextSynchronizationContextMessageSink(nextSink);
            }

            public string Name { get { return "ContributeInstallContextSynchronizationContextMessageSink"; } }

            public bool IsNewContextOK(Context ctx)
            {
                return true;
            }

            public void Freeze(Context ctx)
            {
            }
        }

        sealed class InstallContextSynchronizationContextMessageSink : IMessageSink
        {
            readonly IMessageSink m_NextSink;

            public InstallContextSynchronizationContextMessageSink(IMessageSink nextSink)
            {
                m_NextSink = nextSink;
            }

            public IMessageSink NextSink
            {
                get { return m_NextSink; }
            }

            public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
            {
                var contextSyncContext = new ContextSynchronizationContext(SynchronizationContext.Current);
                var syncContextReplacer = new SynchronizationContextReplacer(contextSyncContext);

                DelegateMessageSink.SyncProcessMessageDelegate replySyncDelegate = (n, m) => SyncProcessMessageDelegateForAsyncReply(n, m, syncContextReplacer);
                var newReplySink = new DelegateMessageSink(replySink, replySyncDelegate, null);
                return m_NextSink.AsyncProcessMessage(msg, newReplySink);
            }

            public IMessage SyncProcessMessage(IMessage msg)
            {
                var contextSyncContext = new ContextSynchronizationContext(SynchronizationContext.Current);
                using (new SynchronizationContextReplacer(contextSyncContext))
                {
                    var ret = m_NextSink.SyncProcessMessage(msg);
                    return ret;
                }
            }

            private IMessage SyncProcessMessageDelegateForAsyncReply(IMessageSink nextSink, IMessage msg, SynchronizationContextReplacer syncContextReplacer)
            {
                syncContextReplacer.Dispose();
                return nextSink.SyncProcessMessage(msg);
            }

            private void PreChecks()
            {
                if (SynchronizationContext.Current != null)
                    return;

                if (TaskScheduler.Current != TaskScheduler.Default)
                    throw new InvalidOperationException("InstallContextSynchronizationContextMessageSink does not support calling methods with SynchronizationContext.Current as null while Taskscheduler.Current is not TaskScheduler.Default");
            }
        }

        sealed class SynchronizationContextReplacer : IDisposable
        {
            SynchronizationContext m_original;
            SynchronizationContext m_new;

            public SynchronizationContextReplacer(SynchronizationContext syncContext)
            {
                m_original = SynchronizationContext.Current;
                m_new = syncContext;
                SynchronizationContext.SetSynchronizationContext(m_new);
            }

            public void Dispose()
            {
                // We don't expect the SynchronizationContext to be changed during the lifetime of the SynchronizationContextReplacer
                if (SynchronizationContext.Current != m_new)
                    throw new InvalidOperationException("SynchronizationContext was changed unexpectedly.");

                SynchronizationContext.SetSynchronizationContext(m_original);
            }
        }

        sealed class ContextSynchronizationContext : PassThroughSynchronizationConext
        {
            readonly Context m_context;

            private ContextSynchronizationContext(SynchronizationContext passThroughSyncContext, Context ctx)
                : base(passThroughSyncContext)
            {
                if (ctx == null)
                    throw new ArgumentNullException("ctx");

                m_context = ctx;
            }

            public ContextSynchronizationContext(SynchronizationContext passThroughSyncContext)
                : this(passThroughSyncContext, Thread.CurrentContext)
            {
            }

            protected override SynchronizationContext CreateCopy(SynchronizationContext copiedPassThroughSyncContext)
            {
                return new ContextSynchronizationContext(copiedPassThroughSyncContext, m_context);
            }

            protected override void CreateSendOrPostCallback(SendOrPostCallback d, object state)
            {
                CrossContextDelegate ccd = () => d(state);
                m_context.DoCallBack(ccd);
            }
        }

        abstract class PassThroughSynchronizationConext : SynchronizationContext
        {
            readonly SynchronizationContext m_passThroughSyncContext;

            protected PassThroughSynchronizationConext(SynchronizationContext passThroughSyncContext)
                : base()
            {
                m_passThroughSyncContext = passThroughSyncContext;
            }

            protected abstract void CreateSendOrPostCallback(SendOrPostCallback d, object state);
            protected abstract SynchronizationContext CreateCopy(SynchronizationContext copiedPassThroughSyncContext);

            public sealed override void Post(SendOrPostCallback d, object state)
            {
                var d2 = CreateSendOrPostCallback(d);
                if (m_passThroughSyncContext != null)
                    m_passThroughSyncContext.Post(d2, state);
                else
                    base.Post(d2, state);
            }

            public sealed override void Send(SendOrPostCallback d, object state)
            {
                var d2 = CreateSendOrPostCallback(d);
                if (m_passThroughSyncContext != null)
                    m_passThroughSyncContext.Send(d2, state);
                else
                    base.Send(d2, state);
            }

            public sealed override SynchronizationContext CreateCopy()
            {
                var copiedSyncCtx = m_passThroughSyncContext != null ? m_passThroughSyncContext.CreateCopy() : null;
                return CreateCopy(copiedSyncCtx);
            }

            public sealed override void OperationCompleted()
            {
                if (m_passThroughSyncContext != null)
                    m_passThroughSyncContext.OperationCompleted();
                else
                    base.OperationCompleted();
            }

            public sealed override void OperationStarted()
            {
                if (m_passThroughSyncContext != null)
                    m_passThroughSyncContext.OperationStarted();
                else
                    base.OperationStarted();
            }

            public sealed override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
            {
                return m_passThroughSyncContext != null ?
                    m_passThroughSyncContext.Wait(waitHandles, waitAll, millisecondsTimeout) :
                    base.Wait(waitHandles, waitAll, millisecondsTimeout);
            }

            private SendOrPostCallback CreateSendOrPostCallback(SendOrPostCallback d)
            {
                SendOrPostCallback sopc = s => CreateSendOrPostCallback(d, s);
                return sopc;
            }
        }

        sealed class DelegateMessageSink : IMessageSink
        {
            public delegate IMessage SyncProcessMessageDelegate(IMessageSink nextSink, IMessage msg);
            public delegate IMessageCtrl AsyncProcessMessageDelegate(IMessageSink nextSink, IMessage msg, IMessageSink replySink);

            readonly IMessageSink m_NextSink;
            readonly SyncProcessMessageDelegate m_syncProcessMessageDelegate;
            readonly AsyncProcessMessageDelegate m_asyncProcessMessageDelegate;

            public DelegateMessageSink(IMessageSink nextSink, SyncProcessMessageDelegate syncProcessMessageDelegate, AsyncProcessMessageDelegate asyncProcessMessageDelegate)
            {
                m_NextSink = nextSink;
                m_syncProcessMessageDelegate = syncProcessMessageDelegate;
                m_asyncProcessMessageDelegate = asyncProcessMessageDelegate;
            }

            public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
            {
                return (m_asyncProcessMessageDelegate != null) ?
                    m_asyncProcessMessageDelegate(m_NextSink, msg, replySink) :
                    m_NextSink.AsyncProcessMessage(msg, replySink);
            }

            public IMessageSink NextSink
            {
                get { return m_NextSink; }
            }

            public IMessage SyncProcessMessage(IMessage msg)
            {
                return (m_syncProcessMessageDelegate != null) ?
                    m_syncProcessMessageDelegate(m_NextSink, msg) :
                    m_NextSink.SyncProcessMessage(msg);
            }
        }
    }

Short answer: Remoting calls do not work on private fields. The async/await rewriting causes an attempt to make a remoting call on a private field.

The issue can be reproduced without async/await. And demonstrating it this way is helpful in understanding what is going on in the async/await case:

[OhMy]
public class Class2 : ContextBoundObject
{
    private string one = "1";

    public void Method1()
    {
        var nc = new NestedClass(this);
    }

    public class NestedClass
    {
        public NestedClass(Class2 c2)
        {
            Console.WriteLine(c2.one);  // Note: nested classes are allowed access to outer classes privates
        }
    }
}

static void Main(string[] args)
{
    var c2 = new Class2();

    // This call causes no problems:
    c2.Method1();

    // This, however, causes the issue.
    var nc = new Class2.NestedClass(c2);
}

Let's walk through what happens line by line:

  1. In Main, we start out in Context0
  2. Since Class2 is a ContextBoundObject and since the OhMyAttribute considers the current context unacceptable, an instance of Class2 is created in Context1 (I'll call this c2_real, and what is returned and stored in c2 is a remoting proxy to c2_real.
  3. When c2.Method1() is called, it is called on the remote proxy. Since we are in Context0, the remote proxy realizes it is not in the correct context so it switches to Context1, and the code within Method1 is executed. 3.a Within Method1 we call the NestedClass constructor which uses c2.one. In this case, we are already in Context1, so the c2.one requires no context switches and so we are using the c2_real object directly.

Now, the problematic case:

  1. We create a new NestedClass passing in the remote proxy c2. No context switches occur here because NestedClass is not a ContextBoundObject.
  2. Within the NestedClass ctor, it access c2.one. The remote proxy notices that we are still in Context0, and so it attempts to remote this call to Context1. This fails because c2.one is a private field. You'll see in Object.GetFieldInfo it is only looking for Public fields:

    private FieldInfo GetFieldInfo(String typeName, String fieldName)
    {
        // ...
    
        FieldInfo fldInfo = t.GetField(fieldName, BindingFlags.Public | 
                                                    BindingFlags.Instance | 
                                                    BindingFlags.IgnoreCase);
        if(null == fldInfo)
        {
    #if FEATURE_REMOTING 
            throw new RemotingException(String.Format(
                CultureInfo.CurrentCulture, Environment.GetResourceString("Remoting_BadField"),
                                                fieldName, typeName));            
        // ...
    
        }
    
        return fldInfo;
    }
    

So, How does async/await end up causing this same issue?

The async/await causes your Class1 to get rewritten such that it uses a nested class with a state machine (used ILSpy to generate):

public class Class1 : ContextBoundObject
{
    // ...
    private struct <Method1>d__0 : IAsyncStateMachine
    {
        public int <>1__state;
        public AsyncTaskMethodBuilder <>t__builder;
        public Class1 <>4__this;
        private TaskAwaiter <>u__$awaiter1;
        private object <>t__stack;

        void IAsyncStateMachine.MoveNext()
        {
            try
            {
                int num = this.<>1__state;
                if (num != -3)
                {
                    TaskAwaiter taskAwaiter;
                    if (num != 0)
                    {
                        Console.WriteLine(this.<>4__this.one);
                        taskAwaiter = Task.Delay(50).GetAwaiter();
                        if (!taskAwaiter.IsCompleted)
                        {
                            this.<>1__state = 0;
                            this.<>u__$awaiter1 = taskAwaiter;
                            this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Class1.<Method1>d__0>(ref taskAwaiter, ref this);
                            return;
                        }
                    }
                    else
                    {
                        taskAwaiter = this.<>u__$awaiter1;
                        this.<>u__$awaiter1 = default(TaskAwaiter);
                        this.<>1__state = -1;
                    }
                    taskAwaiter.GetResult();
                    taskAwaiter = default(TaskAwaiter);
                    Console.WriteLine(this.<>4__this.one);
                }
            }
            catch (Exception exception)
            {
                this.<>1__state = -2;
                this.<>t__builder.SetException(exception);
                return;
            }
            this.<>1__state = -2;
            this.<>t__builder.SetResult();
        }

        // ... 
    }

    private string one = "1";

    public Task Method1()
    {
        Class1.<Method1>d__0 <Method1>d__;
        <Method1>d__.<>4__this = this;
        <Method1>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
        <Method1>d__.<>1__state = -1;
        AsyncTaskMethodBuilder <>t__builder = <Method1>d__.<>t__builder;
        <>t__builder.Start<Class1.<Method1>d__0>(ref <Method1>d__);
        return <Method1>d__.<>t__builder.Task;
    }
}

The important thing to notice is that

  • It's created a nested structure which has access to the privates of Class1
  • The this variable is lifted and stored in the nested class.

So, what happens here is that

  1. On the initial call to c1.Method1() the remoting proxy notices we are in Context0, and that it needs to switch to Context1.
  2. Eventually, MoveNext is called, and c1.one is called. Since we are already in Context1, no context switch is necessary (so the issue doesn't occur).
  3. Later, since a continuation was registered, a call to MoveNext will occur again to execute the rest of the code after the await. However, this call to MoveNext will not occur inside a call to one of Class1's methods. Thus, when the code c1.one is executed this time, we will be in Context0. The remoting proxy notices we are in Context0, and attempts a context switch. This causes the same failure as above since c1.one is a private field.

Workaround: I'm not sure of a general workaround, but for this specific case you can work around the issue by not using the this reference in the method. I.e.:

public async Task Method1()
{
    var temp = one;
    Console.WriteLine(temp);
    await Task.Delay(50);
    Console.WriteLine(temp);
}

Or switch to using a private property instead of a field.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!