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); }
让我们逐行了解一下:
- 在Main中,我们从Context0开始
- 由于
Class2
是ContextBoundObject
并且由于OhMyAttribute
认为当前上下文不可接受,因此在Context1中创建了Class2
的实例(我将调用此c2_real
,并且返回并存储在c2
是c2_real
的远程代理。 - 调用
c2.Method1()
,会在远程代理上调用它。 由于我们在Context0中,远程代理意识到它不在正确的上下文中,所以它切换到Context1,并执行Method1
的代码。 3.a在Method1
我们调用使用c2.one
的NestedClass
构造c2.one
。 在这种情况下,我们已经在Context1中,因此c2.one
不需要上下文切换,因此我们直接使用c2_real
对象。
现在,有问题的案例:
- 我们在远程代理
c2
创建一个新的NestedClass
。 此处不会发生上下文切换,因为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 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
变量被提升并存储在嵌套类中。
那么,这里发生的是
- 在对
c1.Method1()
的初始调用中,远程处理代理通知我们在Context0中,并且需要切换到Context1。 - 最终
MoveNext
,并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); }
或者切换到使用私有属性而不是字段。