Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 693
Krumelur Avatar asked Jul 29 '15 08:07

Krumelur


People also ask

What happens if we execute an asynchronous method but don't await it?

The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.

What happens in an async method?

An async method runs synchronously until it reaches its first await expression, at which point the method is suspended until the awaited task is complete. In the meantime, control returns to the caller of the method, as the example in the next section shows.

What does async do and why is it used with await?

An async function is a function declared with the async keyword, and the await keyword is permitted within it. The async and await keywords enable asynchronousasynchronousAsynchronous programming is a technique that enables your program to start a potentially long-running task and still be able to be responsive to other events while that task runs, rather than having to wait until that task has finished. Once that task has finished, your program is presented with the result.https://developer.mozilla.org › Asynchronous › IntroducingIntroducing asynchronous JavaScript - Learn web development | MDN, promisepromiseIn other cases a future and a promise are created together and associated with each other: the future is the value, the promise is the function that sets the value – essentially the return value (future) of an asynchronous function (promise).https://en.wikipedia.org › wiki › Futures_and_promisesFutures and promises - Wikipedia-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.

What is the benefit of using async?

A significant benefit of the async/await pattern in languages that support it is that asynchronous, non-blocking code can be written, with minimal overhead, and looking almost like traditional synchronous, blocking code.


2 Answers

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));
}
like image 177
Ant P Avatar answered Sep 28 '22 13:09

Ant P


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.

like image 38
Yuval Itzchakov Avatar answered Sep 28 '22 11:09

Yuval Itzchakov