EndInvoke更改当前的CallContext - 为什么?

时间:2009-05-19 15:32:50

标签: .net multithreading

我有以下测试

[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从paralell方法合并到当前的方法 - 但我不明白这个的原因。

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

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

2 个答案:

答案 0 :(得分:7)

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

在.NET Framework源步进的帮助下对此进行调试,对此效果有明确的注释:

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 但不会流出,或者从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线程上运行,因此可以访问相同的线程本地存储。

希望这有助于清理事情。

答案 1 :(得分:0)

这是因为您正在将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"));
}