Задача <T>.Подробность и конкатенация строк

Я играл с async / await, когда натолкнулся на следующее:

class C
{
    private static string str;

    private static async Task<int> FooAsync()
    {
        str += "2";
        await Task.Delay(100);
        str += "4";
        return 5;
    }

    private static void Main(string[] args)
    {
        str = "1";
        var t = FooAsync();
        str += "3";

        str += t.Result; // Line X

        Console.WriteLine(str);
    }
}

Я ожидал, что результатом будет "12345", но это было "1235". Как-то "4" было съедено.

Если я разделил строку X на:

int i = t.Result;
str += i;

Затем ожидаемые результаты "12345".

Почему так? (Использование VS2012)

Ответ 1

Это состояние гонки. Вы не синхронизируете доступ к общей переменной между двумя потоками выполнения, которые у вас есть.

Ваш код, вероятно, сделает что-то вроде этого:

  • устанавливает строку как "1"
  • вызов FooAsync
  • добавить 2
  • Когда ожидание вызвано, основной метод продолжает выполняться, обратный вызов в FooAsync будет запущен в пуле потоков; отсюда все вещи неопределенны.
  • основной поток добавляет 3 к строке

Тогда мы перейдем к интересной строке:

str += t.Result;

Здесь он разбит на несколько меньших операций. Сначала он получит текущее значение str. На данный момент асинхронный метод (по всей вероятности) еще не закончен, поэтому он будет "123". Затем он ждет завершения задачи (потому что Result заставляет ждать блокировки) и добавляет результат задачи, в этом случае 5 в конец строки.

Асинхронный обратный вызов будет схвачен и перезаписан str после того, как основной поток уже захватил текущее значение str, а затем перезапишет str, не прочитав его, поскольку основной поток скоро чтобы перезаписать его.

Ответ 2

Почему так? (Использование VS2012)

Вы запускаете это в консольном приложении, что означает, что текущий контекст синхронизации отсутствует.

Таким образом, часть метода FooAsync() после await выполняется в отдельном потоке. Когда вы выполняете str += t.Result, вы эффективно выполняете условие гонки между вызовом += 4 и += t.Result. Это связано с тем, что string += не является атомной операцией.

Если бы вы запускали тот же код в приложении Windows Forms или WPF, контекст синхронизации был бы захвачен и использован для += "4", что означает, что все они будут выполняться в одном потоке, и вы не увидите этот вопрос.

Ответ 3

Операторы С# формы x += y; расширяются до x = x + y; во время компиляции.

str += t.Result; становится str = str + t.Result;, где str считывается до получения t.Result. На данный момент времени str составляет "123". Когда выполняется продолжение в FooAsync, он изменяет str, а затем возвращает 5. Итак, str теперь "1234". Но тогда значение str, которое было прочитано до продолжения в FooAsync, запущено (которое равно "123"), конкатенировано с 5, чтобы назначить str значение "1235".

Когда вы разбиваете его на два оператора, int i = t.Result; str += i;, этого поведения не может быть.