ContextBoundObject выдает ошибку удаленного взаимодействия после ожидания

У меня есть некоторый код журнала, который был написан для перехвата вызовов методов, используя ContextBoundObject и ContextAttribute. Код основан на примере кода проекта.

Все это работало нормально, пока мы не начали использовать эту библиотеку с кодом, который использует async и await. Теперь мы получаем ошибки удаленного взаимодействия при запуске кода. Вот простой пример, который воспроизводит проблему:

public class OhMyAttribute : ContextAttribute
{
    public OhMyAttribute() : base("OhMy")
    {
    }
}

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

Когда мы призываем Method1 мы получаем следующее RemotingException На втором Console.WriteLine:

Remoting cannot find field 'one' on type 'WindowsFormsApplication1.Class1'.

Есть ли способ обойти эту проблему, используя встроенные методы C#, или мы должны искать альтернативное решение, такое как PostSharp?

2 ответа

Решение

Вот более общий обходной путь.

Имеет следующие недостатки:

  • Это не поддерживает изменение SynchronizationContext в пределах ContextBoundObject, Будет throw в таком случае.
  • Это не поддерживает случай использования await когда SynchronizationContext.Current является нулевым и TaskScheduler.Current это не TaskScheduler.Default, В этом случае обычно await захватит TaskScheduler и использовать это, чтобы опубликовать оставшуюся часть работы, но так как это решение устанавливает SynchronizationContext TaskScheduler не будет захвачен. Таким образом, когда эта ситуация обнаружена, она будет throw,
  • Не поддерживает использование .ConfigureAwait(false) так как это приведет к SynchronizationContext не быть пойманным. К сожалению, я не смог обнаружить этот случай. Однако, если пользователь хочет получить .ConfigureAwait(false) как поведение для основного сквозного SynchronizationContext, они могут использовать пользовательский ожидающий (см. /questions/36703082/ozhidaetsya-li-ozhidanie-vosstanovleniya-threadcurrentcontext/36703096#36703096).

Одна интересная вещь здесь - то, что я попытался создать "проход" SynchronizationContext, То есть я не хотел перезаписывать существующие SynchronizationContext, а лучше сохраните его поведение и наложите поверх него поведение выполнения работы в надлежащем контексте. Любые комментарии о лучшем подходе приветствуются.

    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);
            }
        }
    }

Краткий ответ: удаленные вызовы не работают на частных полях. async / await перезапись вызывает попытку сделать удаленный вызов на приватном поле.

Проблема может быть воспроизведена без async / await, И демонстрация этого таким образом помогает понять, что происходит в async / await дело:

[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);
}

Давайте посмотрим, что происходит построчно:

  1. В основном мы начинаем в Context0
  2. поскольку Class2 это ContextBoundObject и так как OhMyAttribute считает текущий контекст неприемлемым, например, Class2 создается в Context1 (я назову это c2_real и что возвращается и хранится в c2 является удаленным прокси c2_real,
  3. когда c2.Method1() называется, он вызывается на удаленном прокси. Поскольку мы находимся в Context0, удаленный прокси-сервер понимает, что он находится не в правильном контексте, поэтому он переключается на Context1 и код внутри Method1 выполнен. 3.a в пределах Method1 мы называем NestedClass конструктор, который использует c2.one, В этом случае мы уже находимся в Context1, поэтому c2.one не требует переключений контекста, и поэтому мы используем c2_real объект напрямую.

Теперь проблемный случай:

  1. Мы создаем новый NestedClass переходя в удаленный прокси c2, Здесь не происходит переключений контекста, потому что NestedClass это не ContextBoundObject,
  2. В пределах NestedClass ctor, это доступ к c2.one. Удаленный прокси-сервер замечает, что мы все еще находимся в Context0, и поэтому он пытается удалить этот вызов Context1. Это не удается, потому что c2.one это личное поле. Вы увидите в Object.GetFieldInfo он ищет только открытые поля:

    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;
    }
    

Итак, как же async / await в конечном итоге вызывает эту же проблему?

async / await вызывает ваш Class1 чтобы переписать его так, чтобы он использовал вложенный класс с конечным автоматом (для генерации использовал ILSpy):

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;
    }
}

Важно отметить, что

  • Он создал вложенную структуру, которая имеет доступ к частным лицам Class1
  • this переменная поднимается и сохраняется во вложенном классе.

Итак, что здесь происходит, что

  1. При первом звонке c1.Method1() удаленный прокси-сервер замечает, что мы находимся в Context0, и что он должен переключиться на Context1.
  2. В конце концов, MoveNext называется, и c1.one называется. Поскольку мы уже находимся в Context1, переключение контекста не требуется (поэтому проблема не возникает).
  3. Позже, так как продолжение было зарегистрировано, звонок MoveNext произойдет снова, чтобы выполнить остальную часть кода после await, Тем не менее, этот призыв к MoveNext не будет происходить внутри звонка одному из Class1 методы. Таким образом, когда код c1.one выполняется на этот раз, мы будем в Context0. Удаленный прокси-сервер замечает, что мы находимся в Context0, и пытается переключить контекст. Это вызывает тот же сбой, что и выше, так как c1.one это личное поле.

Обходной путь : я не уверен в общем обходном пути, но для этого конкретного случая вы можете обойти проблему, не используя this ссылка в методе. То есть:

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

Или переключитесь на использование частной собственности вместо поля.

Другие вопросы по тегам