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:
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
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.
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.
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.
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.
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));
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With