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

时间:2014-03-13 23:14:48

标签: c# async-await custom-attributes

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

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

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 个答案:

答案 0 :(得分:7)

简答:远程调用不适用于私有字段。 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. 开始
  3. 由于Class2ContextBoundObject,并且由于OhMyAttribute认为当前上下文不可接受,因此在{strong> Context1 中创建Class2的实例(我将其称为c2_real,并且c2中返回并存储的内容是c2_real的远程代理。
  4. 调用c2.Method1()时,会在远程代理上调用它。由于我们在Context0中,远程代理意识到它不在正确的上下文中,因此它切换到Context1,并执行Method1中的代码。 3.a在Method1内,我们调用使用NestedClass的{​​{1}}构造函数。在这种情况下,我们已经在Context1中,因此c2.one不需要上下文切换,因此我们直接使用c2.one对象。
  5. 现在,有问题的案例:

    1. 我们在远程代理c2_real中创建新的NestedClass传递。此处不会发生上下文切换,因为c2不是NestedClass
    2. ContextBoundObject ctor中,它访问c2.one。远程代理注意到我们仍然在Context0中,因此它尝试远程调用Context1。此操作失败,因为NestedClass是私有字段。您会在c2.one中看到它只是在寻找公共字段:

      Object.GetFieldInfo
    3. 那么, 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被重写,以便它使用带状态机的嵌套类(使用ILSpy生成):

      Class1

      需要注意的重要事项是

      • 它创建了一个嵌套结构,可以访问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变量被提升并存储在嵌套类中。

      所以,这里发生的是

      1. 在初始调用this时,远程代理通知我们在Context0中,并且需要切换到Context1。
      2. 最终调用c1.Method1(),并调用MoveNext。由于我们已经在Context1中,因此不需要进行上下文切换(因此不会出现问题)。
      3. 稍后,由于注册了延续,因此将再次调用c1.one以执行MoveNext之后的其余代码。但是,在调用await方法之一时,不会发生对MoveNext的调用。因此,当这次执行代码Class1时,我们将在Context0中。远程代理通知我们在Context0中,并尝试上下文切换。这导致与上述相同的失败,因为c1.one是私有字段。
      4. 解决方法: 我不确定一般的解决方法,但对于这种特定情况,您可以通过不使用方法中的c1.one引用来解决此问题。即:

        this

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

答案 1 :(得分:2)

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

它有以下不足之处:

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

这里有一件有趣的事情是我试图创建一个“传递”SynchronizationContext。也就是说,我不想覆盖任何现有的SynchronizationContext,而是保留其行为和层次,以便在适当的上下文中完成工作。欢迎任何关于更好方法的评论。

SynchronizationContext