Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deal with side effects produced by async/await when it comes to mutable value types?

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?

like image 401
mark Avatar asked Jul 17 '14 18:07

mark


People also ask

What happens when async method is not awaited?

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.

Does async await create a new thread?

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.

What is the return type of async await in C#?

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.

Can async return void?

Event handlers naturally return void, so async methods return void so that you can have an asynchronous event handler.


1 Answers

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.

like image 88
Servy Avatar answered Oct 06 '22 19:10

Servy