ContextBoundObject在等待之后引发远程处理错误

我有一些日志代码,使用ContextBoundObject和ContextAttribute编写拦截方法调用。 该代码基于代码项目示例 。

这一切都运行良好,直到我们开始使用此库与利用async和等待的代码。 现在我们在运行代码时遇到了远程错误。 这是一个重现问题的简单示例:

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我们在第二个Console.WriteLine上得到以下RemotingException

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

有没有办法使用内置的C#方法解决这个问题,还是我们必须看看像PostSharp这样的替代解决方案?

这是一个更一般的解决方法。

它有以下不足之处:

  • 它不支持在ContextBoundObject更改SynchronizationContext 。 它将throw这种情况。
  • SynchronizationContext.Current为null 并且 TaskScheduler.Current不是TaskScheduler.Default时,它不支持使用await的情况。 在这种情况下,通常await将捕获TaskScheduler并使用它来发布剩余的工作,但由于此解决方案设置SynchronizationContext ,因此不会捕获TaskScheduler 。 因此,当检测到这种情况时,它会throw
  • 它不支持使用.ConfigureAwait(false)因为这将导致不捕获SynchronizationContext 。 不幸的是,我无法发现这种情况。 但是,如果用户确实希望得到.ConfigureAwait(false)就像底层传递SynchronizationContext行为一样,他们可以使用自定义awaiter(请参阅https://stackoverflow.com/a/22417031/495262 )。

这里有一件有趣的事情是我试图创建一个“传递” 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 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. 在Main中,我们从Context0开始
  2. 由于Class2ContextBoundObject并且由于OhMyAttribute认为当前上下文不可接受,因此在Context1中创建了Class2的实例(我将调用此c2_real ,并且返回并存储在c2c2_real的远程代理。
  3. 调用c2.Method1() ,会在远程代理上调用它。 由于我们在Context0中,远程代理意识到它不在正确的上下文中,所以它切换到Context1,并执行Method1的代码。 3.a在Method1我们调用使用c2.oneNestedClass构造c2.one 。 在这种情况下,我们已经在Context1中,因此c2.one不需要上下文切换,因此我们直接使用c2_real对象。

现在,有问题的案例:

  1. 我们在远程代理c2创建一个新的NestedClass 。 此处不会发生上下文切换,因为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 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.AwaitUnsafeOnCompletedd__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.d__0 d__; d__.<>4__this = this; d__.<>t__builder = AsyncTaskMethodBuilder.Create(); d__.<>1__state = -1; AsyncTaskMethodBuilder <>t__builder = d__.<>t__builder; <>t__builder.Startd__0>(ref d__); return d__.<>t__builder.Task; } } 

需要注意的重要一点是

  • 它创建了一个嵌套结构,可以访问Class1
  • this变量被提升并存储在嵌套类中。

那么,这里发生的是

  1. 在对c1.Method1()的初始调用中,远程处理代理通知我们在Context0中,并且需要切换到Context1。
  2. 最终MoveNext ,并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); } 

或者切换到使用私有属性而不是字段。