Для тех, кто, как я, не смог сразу же найти это из объяснения / кода, вот краткое резюме:
zero
может оставить только внутренний цикл while, если turn == 0
. Затем он устанавливает turn
на 1
или 2
.
even
может оставить только внутренний цикл while, если turn == 2
. Затем он устанавливает turn
в 0
.
Намерение состоит в том, что odd
может выйти из внутреннего цикла while, только если turn == 1
(тогда он устанавливает turn
в 0
.), Ноэто не реализовано правильно.
Поскольку условия выхода из циклов вращения являются (должны быть) взаимоисключающими, не более одного потока должно быть вне его собственного цикла вращения в данный момент времени, поэтому одновременных модификаций не должно быть. должно быть возможно (и программа должна быть свободна от гонки).
Проблема в том, что turn == 0 || turn == 2
не является атомарным. Может произойти следующее:
zero
завершает одну итерацию и устанавливает turn = 2
.
odd
проверок turn == 0
, чтоложно.
Между тем, even
также видит turn == 2
, выходит из цикла вращения, завершает итерацию и устанавливает turn = 0
.
odd
теперь проверяет правую часть оператора ||
: turn == 2
, что неверно.
odd
выходит из цикла вращения (даже еслиturn == 0
!) , что, конечно, непреднамеренно и приводит к гонке от нуля до нечетного.
Короче говоря, проблема в том, что левая часть ||
может быть ложным, но станет истинным к тому времени, когда будет оценена правая часть. Ни в одном из вышеприведенных пунктов не было целого turn == 0 || turn == 2
, равного false
, если оценивать его атомарно, но, поскольку оно не является атомарным, вы получили «смесь» из false || true
и true || false
(а именно false || false
).
Выражение turn <= 2 && turn != 1
не имеет этой проблемы, поскольку первое условие всегда true
, а второе условие - это проверка, которую вы действительно хотите.
В общем случае решение состоит в том, чтобы один раз прочитать атомное в локальную tmp и проверить это. Это лучше для производительности, потому что позволяет компилятору совместно оптимизировать ваши условия или совместно оптимизировать то, что вы собираетесь делать.
while (true) {
int t = turn;
if (!(t == 0 || t == 2)) break;
yield();
}
Или, возможно, взломать это в одну строку с помощью оператора запятой. Запятая является «точкой последовательности», поэтому вы можете присвоить t
и затем прочитать ее. Но это не очень удобно для восприятия человеком.
int tmp;
while (tmp=turn, (tmp == 0 || tmp == 2)) {
yield();
}
Если вы действительно хотите подождать, пока turn
будет нечетным, вы можете использовать turn % 2 == 0
или
while ( turn&1 == 0 )
yield();