Как можно синхронизировать чтение в небуферизованном канале перед записью в канал?

В документации модели памяти Golang указано следующее:

(1)

Некоторые операции с памятью аналогичны чтению, включая чтение, атомарное чтение, блокировку мьютекса и прием канала. Другие операции с памятью аналогичны записи, включая запись, атомарную запись, разблокировку мьютекса, отправку канала и закрытие канала.

(2)

Отношение «синхронизировано перед» — это частичный порядок синхронизации операций с памятью, производный от W. Если синхронизирующая операция чтения памяти r наблюдает за синхронизирующей операцией записи памяти w (то есть, если W(r) = w), то w синхронизируется перед r.

(3)

Прием из небуферизованного канала синхронизируется до завершения соответствующей отправки по этому каналу.

Определение отношения синхронизации перед (2) гласит, что если синхронизирующая операция чтения памяти r наблюдает за синхронизирующей операцией записи w, то w синхронизируется раньше r.

В документации также указано (3), что прием из небуферизованного канала синхронизируется перед отправкой по этому каналу.

Однако меня смущает то, что если получение из канала является операцией чтения (1), как его можно синхронизировать перед соответствующей записью. Согласно определению соотношения «синхронизироваться перед» (2), все может быть только наоборот, как в случае с буферизованным каналом, где отправка синхронизируется перед приемом.

Это противоречит определению соотношения синхронизации перед (2).

Я что-то пропустил?

Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
API ввода вопросов - это полезный инструмент для интеграции моделей машинного обучения, таких как ChatGPT, в приложения, требующие обработки...
1
0
117
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Модель памяти также говорит:

Отправка по каналу синхронизируется до завершения соответствующего приема из этого канала.

Прием, подобный чтению, в канале наблюдает за отправкой, подобной записи, по каналу, как описано в (2).

Перефразируя (3), горутина отправки не продолжается до тех пор, пока не будет получено значение. Здесь не говорится, что «прием из небуферизованного канала синхронизируется перед отправкой по этому каналу». Противоречия с (2) нет.

Ответ принят как подходящий

Чего вам, вероятно, не хватает, так это того, что «Прием из небуферизованного канала синхронизируется до завершения соответствующей отправки по этому каналу».

Давайте рассмотрим пример из модели памяти ( Go Playground):

package main

func main() {
    c := make(chan int)
    var a string

    go func() {
        a = "hello, world"
        <-c // (1)
        // (2)
    }()

    c <- 0 // (3)
    // (4)
    println(a)
}
  • Если синхронизирующая операция чтения памяти r наблюдает за синхронизирующей операцией записи памяти w (то есть, если W(r) = w), то w синхронизируется раньше r.
    • (3) синхронизируется раньше (1), мы не можем прочитать значение до его записи — в нашем контексте это неважно
  • Отправка по каналу синхронизируется до завершения соответствующего приема из этого канала.
    • (3) синхронизируется раньше (2), горутина не может выйти до отправки значения - неважно в нашем контексте
  • Прием из небуферизованного канала синхронизируется до завершения соответствующей отправки по этому каналу.
    • (1) синхронизируется перед (4), a = "hello, world" упорядочивается до (1) и println(a) после (4), поэтому println(a) наблюдает эффекты a = "hello, world" без условий гонки.

Редактировать:

Имейте в виду, что «x синхронизировано до y» не означает, что y выполняется сразу после x, а только позже. Вы можете разместить что-нибудь между ними.

Итак, в приведенном выше примере, если у вас есть только один процессор, (3) .. (1) .. (2) .. (4) допустимо, или (3) .. (1) .. (4) . . (2), и вы можете помещать элементы из любой другой горутины, которая работает одновременно в .., например (3) (x) (1) (y) (2) (z) (4) (t). Вам нужно только сохранить указанный порядок.

Вы также можете вписать println(a), если (4) > println(a) из-за последовательности. Итак, (3) (1) (4) println(a) (2) справедливо, как и (3) (1) (4) (2) println(a).

(1) располагается перед (2), поэтому (2)... (1) недопустимо.

Если мы сделаем канал буферизованным

package main

func main() {
    c := make(chan int, 1)
    var a string

    go func() {
        a = "hello, world"
        <-c // (1)
        // (2)
    }()

    c <- 0 // (3)
    // (4)
    println(a)
}

Мы теряем фразу «(1) синхронизируется раньше (4)», поскольку это происходит из «Прием из небуферизованного канала синхронизируется до завершения соответствующей отправки по этому каналу».

Вместо этого мы получаем

  • k-й прием по каналу с пропускной способностью C синхронизируется до завершения k+C-й отправки из этого канала.

Что в нашем случае вообще ничего не значит, так как второй отправки нет.

Итак, поскольку у нас осталось только (3) > (1) и (3) > (2) (в сочетании с (1) > (2) и (3) > (4) из-за последовательности) (3) .. (1) .. (4) .. (2) еще в силе, но (3) .. (4) .. (1) .. (2) теперь тоже. Это означает, что мы можем наблюдать или не наблюдать эффект a = ... в точке println(a), что является состоянием гонки.


Редактировать2:

Для получения дополнительной теоретической основы вы можете прочитать о часах Лампорта и в записях блога Расса Кокса.

Другие вопросы по теме