为什么在使用异步方法时捕获类范围变量,而在使用Action <t>(内部代码示例)时不捕获?

时间:2015-07-29 08:13:14

标签: c# .net asynchronous task

在遛狗时我正在考虑Action<T>Func<T>Task<T>async/await(是的,书呆子,我知道......)并构建了一点测验在我的脑海里编程并想知道答案是什么。我注意到我不确定结果,所以我创建了两个简单的测试。

以下是设置:

  • 我有一个类范围变量(字符串)。
  • 为其分配初始值。
  • 该变量作为参数传递给类方法。
  • 该方法不会直接执行,而是分配给“行动”。
  • 在执行操作之前,我更改了变量的值。

输出是什么?初始值,或更改后的值?

有点令人惊讶但可以理解,输出是改变的值。我的解释是:在动作执行之前,变量不会被压入堆栈,因此它将是已更改的变量。

public class foo
{
    string token;

    public foo ()
    {
        this.token = "Initial Value";
    }

    void DoIt(string someString)
    {
        Console.WriteLine("SomeString is '{0}'", someString);
    }

    public void Run()
    {
        Action op = () => DoIt(this.token);
        this.token = "Changed value";
        // Will output  "Changed value".
        op();
    }
}

接下来,我创建了一个变体:

public class foo
{
    string token;

    public foo ()
    {
        this.token = "Initial Value";
    }

    Task DoIt(string someString)
    {
        // Delay(0) is just there to try if timing is the issue here - can also try Delay(1000) or whatever.
        return Task.Delay(0).ContinueWith(t => Console.WriteLine("SomeString is '{0}'", someString));
    }

    async Task Execute(Func<Task> op)
    {
        await op();
    }

    public async void Run()
    {
        var op = DoIt(this.token);
        this.token = "Changed value";
        // The output will be "Initial Value"!
        await Execute(() => op);
    }
}

我在此DoIt()返回Taskop现在是Task,不再是ActionExecute()方法等待任务。令我惊讶的是,输出现在是&#34;初始值&#34;。

为什么表现不同?

DoIt()Execute()被调用之前不会被执行,那么为什么它会捕获token的初始值?

完成测试:https://gist.github.com/Krumelur/c20cb3d3b4c44134311fhttps://gist.github.com/Krumelur/3f93afb50b02fba6a7c8

3 个答案:

答案 0 :(得分:7)

你有一些误解。首先,当您调用DoIt时,它将返回已经开始执行的任务。执行不会仅在您await任务时启动。

当您重新分配类级字段时,还会在someString变量上创建一个闭包,的值不会改变

Task DoIt(string someString)
{
    return Task.Delay(0).ContinueWith(t 
        => Console.WriteLine("SomeString is '{0}'", someString));
}

传递给Action的{​​{1}}在ContinueWith变量上关闭。请记住,字符串是不可变的,因此,当您重新分配someString的值时,实际上是在分配新字符串引用。但是,token内的局部变量someString会保留旧引用,因此即使在重新分配类字段后,其值仍保持不变。

您可以通过直接在类级字段上关闭此操作来解决此问题:

DoIt

答案 1 :(得分:5)

在这两种情况下,你都在关闭。但是,在这两种情况下,你要对不同的东西进行封闭。

在第一种情况下,您使用this上的闭包创建一个匿名方法 - 当您最终执行委托时,它将采用{em>当前值{{ 1}},获取this当前值并使用它。所以你看到修改后的值。

在第二种情况下,this.token没有关闭 - 或者如果是,它并没有产生任何影响。您明确地传递thisthis.token方法只需要对其自己的参数DoIt进行闭包。这会立即(同步)而不是懒惰地发生 - 因此捕获someString的初始值。 this.token实际上执行委托 - 它只等待异步方法的结果。该方法本身已经运行,只有它的异步部分才是异步的 - 在这种情况下,只有await

如果您希望更清楚地看到这一点,请在Console.WriteLine("SomeString is '{0}'", someString)之后添加Thread.Sleep(1000) - 您会在之前看到this.token = "Changed value";打印,甚至可以到达SomeString is 'Initial Value' 1}}。

要使第二个示例与第一个示例相同,您需要做的就是将await再次更改为委托,而不是op - Task。这会再次延迟var op = () => DoIt(this.token);的执行,并导致与第一个示例中相同的闭包。

TL; DR:

行为不同,因为在第一种情况下,您推迟执行DoIt,而在第二种情况下,您立即运行DoIt(this.token)。我的答案中的其他要点也很重要,但这是关键。

答案 2 :(得分:5)

让我们分解每一个案例。

Action<T>开始:

  

我的解释:变量不会被推入堆栈直到   动作执行,因此它将是更改的

这与堆栈无关。编译器从第一个代码片段生成以下内容:

public foo()
{
    this.token = "Initial Value";
}

private void DoIt(string someString)
{
    Console.WriteLine("SomeString is '{0}'", someString);
}

public void Run()
{
    Action action = new Action(this.<Run>b__3_0);
    this.token = "Changed value";
    action();
}

[CompilerGenerated]
private void <Run>b__3_0()
{
    this.DoIt(this.token);
}

编译器从lambda表达式发出一个命名方法。一旦您调用该操作,并且由于我们在同一个类中,this.token是更新的“更改值”。编译器甚至不会将其提升为显示类,因为这是在实例方法中创建和调用的。

现在,对于async方法。有两个状态机正在生成,不利于国家机器的膨胀,并进入相关部分。状态机执行以下操作:

this.<>8__1 = new foo.<>c__DisplayClass4_0();
this.<>8__1.op = this.<>4__this.DoIt(this.<>4__this.token);
this.<>4__this.token = "Changed value";
taskAwaiter = this.<>4__this.Execute(new Func<Task>(this.<>8__1.<Run>b__0)).GetAwaiter();

这里发生了什么? token会传递给DoItFunc<Task>将返回DoIt。该委托包含对旧标记字符串“Initial Value”的引用。请记住,即使我们谈论的是引用类型,它们都是通过值传递的。这实际上意味着现在token方法中存在旧字符串的新存储位置,该位置指向“初始值”。然后,下一行将string更改为“已更改的值”。存储在Func内的op和已更改的evidensapp_polystructures现在指向两个不同的字符串。

当您调用委托时,它将打印初始值,因为evidensapp_seniangcbr任务会存储您较旧的陈旧值。这就是为什么你会看到两种不同的行为。