【问题标题】:EndInvoke changes current CallContext - why?EndInvoke 更改当前 CallContext - 为什么?
【发布时间】:2009-12-02 00:00:04
【问题描述】:

我有以下测试

[Test]
public void aaa()
{
    CallContext.LogicalSetData("aa", "1");

    Action parallelMethod = () => CallContext.LogicalSetData("aa", "2"); 
    var r = parallelMethod.BeginInvoke(null, null); 
    parallelMethod.EndInvoke(r);

    Assert.That(CallContext.LogicalGetData("aa"), Is.EqualTo("1")); 
}

谁能告诉我为什么这个测试在最后一行失败了?

其实我知道为什么 - 因为 EndInvoke 正在将 CallContext 从并行方法合并到当前方法 - 但我不明白这是为什么。

对我来说,这种行为类似于从被调用的方法内部更改方法参数值:-(

编辑:我已将代码示例更改为仅使用 LogicalGetData 和 LogicalSetData。正如您在我的另一个 question 中看到的那样,我想将一些数据传递给另一个线程,但我没想到 EndInvoke() 会用其他线程中更改的值覆盖我的值。

【问题讨论】:

    标签: .net multithreading


    【解决方案1】:

    您的示例说明的行为确实是设计使然。 LogicalCallContext 能够通过异步调用或 .net 远程调用双向流动。如您所见,当您调用 EndInvoke 时,子上下文的 LogicalCallContext 将合并回父上下文。这是有意的,以便远程方法的调用者可以访问远程方法设置的任何值。如果您愿意,您可以使用此功能将数据从孩子那里返回

    在 .NET Framework 源代码步进的帮助下对此进行调试,有明确的 cmets 可以达到此效果:

    在 System.Runtime.Remoting.Proxies.RemotingProxy.Invoke:

        case Message.EndAsync: 
             // This will also merge back the call context
             // onto the thread that called EndAsync
             RealProxy.EndInvokeHelper(m, false);
    

    在 System.Runtime.Remoting.Proxies.RealProxy.EndInvokeHelper 中:

        // Merge the call context back into the thread that
        // called EndInvoke 
        CallContext.GetLogicalCallContext().Merge(
             mrm.LogicalCallContext);
    

    如果您想避免数据合并,很容易跳过,只需避免从主线程调用 EndInvoke。例如,您可以使用 ThreadPool.QueueUserWorkItem,它将 LogicalCallContext in 流入但不会流出,或者从 AsyncCallback 调用 EndInvoke。

    查看 Microsoft Connect 站点上的示例,您没有看到 LogicalSetData 值从 RunWorkerCompleted 调用流回的原因是 BackgroundWorker 没有将上下文流回。另外,请记住,LogicalSetData 与线程本地存储不同,因此 RunWorkerCompleted 恰好在 UI 线程上运行并不重要——LogicalCallContext 仍然有一个子上下文,除非父级显式将其流回通过从生成线程调用 EndInvoke,它将被放弃。如果您想要线程本地存储,您可以从 Thread 访问它,如下所示:

        private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            Thread.SetData(Thread.GetNamedDataSlot("foo"), "blah!!");
        }
    
        private void button1_Click(object sender, EventArgs e)
        {
            var val = (string)Thread.GetData(Thread.GetNamedDataSlot("foo"));
            MessageBox.Show(val ?? "no value");
        }
    

    此示例弹出一个显示“blah!!”的 MessageBox。原因是两个回调都在 UI 线程上运行,因此可以访问同一个线程本地存储。

    希望这有助于解决问题。

    【讨论】:

      【解决方案2】:

      这是因为您将 SetData/GetData 与 LogicalSetData/LogicalGetData 混合在一起。有一个article,您可以阅读更多关于这两种方法之间差异的信息。这里的经验法则是始终将 SetData 与 GetData 结合使用,将 LogicalSetData 与 LogicalGetData 结合使用。

      此修改将使您的测试通过:

      [Test]
      public void aaa()
      {
          CallContext.SetData("aa", "1");
          Action parallelMethod = () => CallContext.SetData("aa", "2");
          var r = parallelMethod.BeginInvoke(null, null);
          Assert.That(CallContext.GetData("aa"), Is.EqualTo("1"));
          parallelMethod.EndInvoke(r);
          Assert.That(CallContext.GetData("aa"), Is.EqualTo("1"));
      }
      

      【讨论】:

      • @darin 我已将代码示例更改为使用逻辑方法。对不起,但这不是我问题的重点。我无法理解这种奇怪行为背后的逻辑。怎么会有人想用其他线程中更改的数据覆盖本地数据?
      • 如果你想要每个线程的上下文存储,你必须使用 SetData/GetData。使用 LogicalSetData 存储的对象将在应用程序域之间流动,即使它们没有实现 ILogicalThreadAffinative。
      • 问题是我想捕获一些数据并将其传递给我的主线程创建的线程,但不想在这个外部线程完成时覆盖它。也许还有其他方法,即使调用 BeginInvoke()/EndInvoke() 的不是我的代码。我愿意接受建议。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多