Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

340
Views
Why is a class scope variable captured when using an async method but not when using an Action<T> (code examples inside)?

While walking the dog I was thinking about Action<T>, Func<T>, Task<T>, async/await (yes, nerdy, I know...) and constructed a little test program in my mind and wondered what the answer would be. I noticed I was unsure about the result, so I created two simple tests.

Here's the setup:

  • I have a class scope variable (string).
  • It gets assigned an initial value.
  • The variable is passed as a parameter to a class method.
  • The method won't be executed directly, but instead is assigned to an 'Action'.
  • Before the action executes, I change the value of the variable.

What would the output be? The initial value, or the changed value?

A little surprising but understandable, the output is the changed value. My explanation: the variable is not pushed onto the stack until the action executes, so it will be the changed one.

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();
    }
}

Next, I created a variation:

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);
    }
}

Here I made DoIt() return a Task. op is now a Task and no longer an Action. The Execute() method awaits the task. To my surprise, the output is now "Initial Value".

Why does it behave differently?

DoIt() won't be executed until Execute() gets called, so why does it capture the initial value of token?

Complete tests: https://gist.github.com/Krumelur/c20cb3d3b4c44134311f and https://gist.github.com/Krumelur/3f93afb50b02fba6a7c8

over 4 years ago · Santiago Trujillo
3 answers
Answer question

0

You have a couple of misconceptions here. Firstly, when you call DoIt, it returns a Task that has already begun execution. Execution doesn't start only when you await the Task.

You also create a closure over the someString variable, the value of which does not change when you reassign the class-level field:

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

The Action passed to ContinueWith closes on the someString variable. Remember that strings are immutable so, when you reassign the value of token, you are actually assigning a new string reference. The local variable someString inside DoIt, however, retains the old reference, so its value remains the same even after the class field is reassigned.

You could solve this problem by instead having this action close over the class-level field directly:

Task DoIt()
{
    return Task.Delay(0).ContinueWith(t 
        => Console.WriteLine("SomeString is '{0}'", this.token));
}
over 4 years ago · Santiago Trujillo Report

0

In both cases, you are making a closure. However, you're making a closure over different things in the two cases.

In the first case, you're making an anonymous method with a closure over this - when you finally execute the delegate, it will take the current value of this, get the current value of this.token and use that. So you see the modified value.

In the second case, there is no closure over this - or if it is, it doesn't make a difference. You pass this.token explicitly, and the DoIt method only needs to make a closure over its own argument, someString. This happens immediately (synchronously), rather than lazily - so the initial value of this.token is captured. await doesn't actually execute the delegate - it only waits for the results of the asynchronous method. The method itself has already ran, and only it's asynchronous part is, well, asynchronous - in this case, only the Console.WriteLine("SomeString is '{0}'", someString).

If you want to see this more clearly, add Thread.Sleep(1000) after this.token = "Changed value"; - you will see SomeString is 'Initial Value' printed out before you even get to the await.

To make the second example behave like the first, all you need to do is change op to be a delegate again, rather than a Task - var op = () => DoIt(this.token);. This delays execution of DoIt again, and causes the same closure as in the first example.

TL;DR:

The behaviour is different because in the first case, you defer the execution of DoIt(this.token), while in the second example you run DoIt(this.token) immediately. The other points in my answer are also important, but this is the crux.

over 4 years ago · Santiago Trujillo Report

0

Let's break down each case.

Starting with the Action<T>:

My explanation: the variable is not pushed onto the stack until the action executes, so it will be the changed one

This hasn't anything to do with the stack. The compiler generates the following from your first code snippet:

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);
}

The compiler emits a named method from your lambda expression. Once you invoke the action, and since we're in the same class, the this.token is the updated "Changed Value". The compiler doesn't even lift this into a display class, since this is all created and invoked inside the instance method.


Now, for the async method. There are two state-machines being generated, ill skimp the bloat of the state-machine and get to the relevant parts. The state-machine does the following:

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();

What happens here? token is passed to DoIt, which will return a Func<Task>. That delegate contains a reference to the old token string, "Initial Value". Remember, even though we're talking about reference types, they are all passed by value. This effectively means that there is a new storage location for the old string now in the DoIt method which points to "Initial Value". Then, the next line changes token to "Changed Value". The string stored inside the Func and the one that was changed are now pointing at two different strings.

When you invoke the delegate, it will print the initial value, as the op task stores your older, stale value. That is why you're seeing two different behaviors.

over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!