Как бороться с побочными эффектами, создаваемыми async/await, когда речь заходит о изменяемых типах значений?

Пожалуйста, рассмотрите следующий пример кода:

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

Обратите внимание на разницу между Test и TestAsync. Этот код удовлетворяет всем утверждениям.

Я думаю, что просмотр кода с Reflector скажет мне, почему, но все же это то, чего я не ожидал вообще.

Конечно, изменение AStruct как класса вместо структуры не приведет к второму утверждению в TestAsync - как я ожидал бы этого в первую очередь.

Мой вопрос в том, что, помимо использования изменчивых структур с async/await, существует ли элегантный способ заставить их мирно сосуществовать?

Ответы

Ответ 1

По своей сути невозможно, чтобы метод async a struct сам мутировал ".

Это, конечно, имеет смысл, когда вы об этом думаете. К тому времени, когда все задачи, которые вы await внутри этой структуры действительно закончили, учитывая, что вы вернулись к вызывающему абоненту и позволили им продолжать делать всевозможные вещи, у вас нет способа гарантировать, что фактический экземпляр структуры, называемый методом, уже существует. Если SetValueAsync вызывается в локальной переменной методом, который не использовал await it или Wait на нем или что-то в этом роде, то это время жизни локальной переменной, вероятно, закончилось к моменту SetValueAsync, чтобы продолжить продолжение он вызывает Run. Он не может мутировать переменную, срок жизни которой может быть или не быть в сфере видимости. Единственный вариант здесь для методов async структуры, чтобы эффективно копировать себя при вызове метода и иметь код в ссылке продолжения, совершенно другую переменную, которая называется переменной async. Поскольку метод создает копию, которая не будет доступна нигде, кроме тела этого метода async, это означает, что для всех целей цели метод async структуры никогда не сможет мутировать эту структуру (и мутация должна быть видна кем-то еще).

У вас может быть метод async изменчивого struct, при условии, что сам этот метод не будет мутировать struct. Этот один метод должен будет вернуть Task<T> с новой структурой или чем-то эквивалентным.

Как интересный такет, он в рамках технической возможности для метода async a struct мутировать себя перед первым await метода, если он действительно этого хотел. Компилятор выбирает копию сразу, поэтому на самом деле это невозможно, но явный выбор был сделан, чтобы сделать копию в самом начале метода, а не только после первого await. Это, вероятно, к лучшему, будь то намеренное решение или нет, так как это было бы супер путать иначе.