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);
}
Давайте посмотрим, что происходит построчно:
- В основном мы начинаем в Context0
- поскольку
Class2
этоContextBoundObject
и так какOhMyAttribute
считает текущий контекст неприемлемым, например,Class2
создается в Context1 (я назову этоc2_real
и что возвращается и хранится вc2
является удаленным проксиc2_real
, - когда
c2.Method1()
называется, он вызывается на удаленном прокси. Поскольку мы находимся в Context0, удаленный прокси-сервер понимает, что он находится не в правильном контексте, поэтому он переключается на Context1 и код внутриMethod1
выполнен. 3.a в пределахMethod1
мы называемNestedClass
конструктор, который используетc2.one
, В этом случае мы уже находимся в Context1, поэтомуc2.one
не требует переключений контекста, и поэтому мы используемc2_real
объект напрямую.
Теперь проблемный случай:
- Мы создаем новый
NestedClass
переходя в удаленный проксиc2
, Здесь не происходит переключений контекста, потому чтоNestedClass
это неContextBoundObject
, В пределах
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
переменная поднимается и сохраняется во вложенном классе.
Итак, что здесь происходит, что
- При первом звонке
c1.Method1()
удаленный прокси-сервер замечает, что мы находимся в Context0, и что он должен переключиться на Context1. - В конце концов,
MoveNext
называется, иc1.one
называется. Поскольку мы уже находимся в Context1, переключение контекста не требуется (поэтому проблема не возникает). - Позже, так как продолжение было зарегистрировано, звонок
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);
}
Или переключитесь на использование частной собственности вместо поля.