Please, consider the following example code:
using System.Diagnostics;
using System.Threading.Tasks;
public struct AStruct
{
public int Value;
public async Task SetValueAsync()
{
Value = await Task.Run(() => 1);
}
public void SetValue()
{
Value = 1;
}
}
class Program
{
static void Main(string[] args)
{
Test(new AStruct());
TestAsync(new AStruct()).Wait();
}
private static async Task TestAsync(AStruct x)
{
Debug.Assert(x.Value == 0);
await x.SetValueAsync();
Debug.Assert(x.Value == 0);
}
private static void Test(AStruct x)
{
Debug.Assert(x.Value == 0);
x.SetValue();
Debug.Assert(x.Value == 1);
}
}
Notice the difference between Test
and TestAsync
. This code satisfies all the assertions.
I guess looking at the code with Reflector will tell me why, but still this is something I did not expect at all.
Of course, changing AStruct
to be a class instead of a struct does fail the second assertion in TestAsync
- as I would expect it in the first place.
My question is this - besides not using mutable structs with async/await is there an elegant way to make them coexist peacefully?
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.
The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active.
Async methods can have the following return types: Task, for an async method that performs an operation but returns no value. Task<TResult>, for an async method that returns a value. void , for an event handler.
Event handlers naturally return void, so async methods return void so that you can have an asynchronous event handler.
It's inherently impossible for an async
method of a struct
to mutate "itself".
This of course makes complete sense when you think about it. By the time the whatever tasks you await
inside of that struct actually finish, given that you've returned to the caller and allowed them to continue on doing all sorts of things, you have no way of ensuring that the actual struct instance that called the method even exists any more. If SetValueAsync
was called on a local variable by a method that didn't await
it or Wait
on it or anything like that then that local variable's lifetime will likely have ended by the time SetValueAsync
reaches the continuation of it's call to Run
. It can't mutate the variable who's lifetime may or may not be in scope. The only option here is for async
methods of a struct to effectively copy themselves when the method is called and have the code in the continuation reference an entirely different variable that the variable that called the async
. Since the method is making a copy that won't be accessible anywhere other than the body of this async
method, it means that, for all intents an purposes, an async
method of a struct can never mutate that struct (and have the mutation be visible by anyone else).
You can have an async
method of a mutable struct
, so long as that method itself isn't expected to mutate the struct
. That one method will need to return a Task<T>
with a new struct, or something equivalent.
As an interesting tanget, it's within the bounds of technical possibility for an async
method of a struct
to mutate itself before the first await
of the method if it really wanted to. The compiler chooses to take the copy right away, so this isn't actually possible, but the explicit choice was made to make the copy at the very start of the method rather than only after the first await
. This is probably for the best, whether it was an intentional decision or not, as it would be super confusing otherwise.
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