Задача <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;
, этого поведения не может быть.