Skip to content

Latest commit

 

History

History

sub1

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

8.4.1 Небуферизованные каналы

Операция отправления в небуферизованный канал блокирует горутину до тех пор, пока другая горутина не выполнит соответствующее получение из того же канала, после чего значение становится переданным, и обе горутины продолжаются. И наоборот, если первой сделана попытка выполнить операцию получения, принимающая горутина блокируется до тех пор, пока другая горутина не выполнит отправление значения в тот же канал.

Связь по небуферизованному каналу приводит к синхронизации операций отправления и получения. По этой причине небуферизованные каналы иногда называют синхронными. Когда значение отправляется в небуферизованный канал, получение значения предшествует продолжению работы отправляющей горутины. При рассмотрении параллелизма, когда мы говорим, что х предшествует у, мы не просто имеем в виду, что х происходит по времени раньше, чем у; мы подразумеваем, что х гарантированно поступает именно так и что все выполненные им предыдущие действия, такие как обновления переменных, завершены и вы можете полностью на них полагаться.

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

Клиентская программа в разделе 8.3 копирует входные данные на сервер в своей главной горутине, так что клиентская программа завершается, как только закрывается входной поток, даже если имеется работающая в фоновом режиме go- подпрограмма. Чтобы перед выходом программа ожидала завершения фоновых горутин, для синхронизации двух горутин мы используем канал (см. netcat3.go):

func main() {
	conn, err := net.Dial("tcp", "localhost:8000")
	if err != nil {
		log.Fatal(err)
	}
	done := make(chan struct{})
	go func() {
		io.Copy(os.Stdout, conn) // Примечание: игнорируем ошибки
		log.Println("done")
		done <- struct{}{} // Сигнал главной горутине
	}()
	mustCopy(conn, os.Stdin)
	conn.Close()
	<-done // Ожидание завершения фоновой горутины
}

func mustCopy(dst io.Writer, src io.Reader) {
	if _, err := io.Copy(dst, src); err != nil {
		log.Fatal(err)
	}
}

Когда пользователь закрывает стандартный входной поток, происходит возврат из функции mustCopy, и главная горутина вызывает conn.Close(), закрывая обе половины сетевого подключения. Закрытие записывающей половины соединения заставляет сервер увидеть признак конца файла. Закрытие считывающей половины приводит к тому, что вызов io.Copy в горутине возвращает ошибку "чтение из закрытого соединения" (поэтому мы убрали запись журнала ошибок); в упражнении 8.3 предлагается лучшее решение. (Обратите внимание, что инструкция go вызывает литерал функции — это весьма распространенная конструкция.) Перед завершением работы фоновая горутина заносит в журнал соответствующее сообщение, а затем отправляет значение в канал done. Главная горутина, до тех пор, пока не получит это значение, находится в состоянии ожидания. В результате перед завершением работы программа всегда заносит в журнал сообщение "done". Сообщения, пересылаемые через каналы, имеют два важных аспекта. Каждое сообщение имеет значение, но иногда важен сам факт передачи сообщения и момент, когда эта передача происходит. Когда мы хотим подчеркнуть этот аспект передачи сообщений, мы называем их событиями. Когда событие не несет дополнительной информации, т.е. его единственная цель — это синхронизация, мы подчеркиваем этот факт, используя канал с типом элементов struct{}, хотя не менее распространено применение для той же цели каналов с типом значений bool или int, поскольку выражение done <- 1 короче выражения done <- struct{}{}.

Выводы:

  • Операция отправления в небуферизованный канал блокирует горутину до тех пор, пока другая горутина не выполнит соответствующее получение из того же канала; это обеспечивает синхронизацию отправления и получения данных между горутинами. Пример:

    ch := make(chan int)
    go func() {
    ch <- 42 // Отправление значения в канал; горутина блокируется, пока другая горутина не выполнит получение
    }()
    val := <-ch // Получение значения из канала; обе горутины продолжают работу
  • Небуферизованные каналы иногда называют синхронными, поскольку они обеспечивают синхронизацию операций отправления и получения; это может быть полезно для упорядочивания выполнения горутин и избегания проблем с параллельным доступом к переменным.

  • Если нужно передать событие без значения, используйте канал с типом элементов struct{} ; это подчеркивает, что важен только факт передачи сообщения. Пример:

    done := make(chan struct{})
    go func() {
        // Работа в фоновой горутине
        done <- struct{}{} // Сигнал главной горутине об окончании работы
    }()
    <-done // Ожидание завершения фоновой горутины
  • Использование каналов для синхронизации горутин обеспечивает явное взаимодействие и гарантирует, что определенные действия завершены до продолжения работы других горутин; это помогает избежать ошибок, связанных с параллельным доступом к общим данным и состоянию.

  • При реализации клиент-серверной связи с использованием горутин и каналов важно использовать синхронизацию для корректного завершения работы программы и предотвращения потери данных. Пример кода:

    func main() {
      conn, err := net.Dial("tcp", "localhost:8000")
      if err != nil {
          log.Fatal(err)
      }
      done := make(chan struct{})
      go func() {
          io.Copy(os.Stdout, conn) // Примечание: игнорируем ошибки
          log.Println("done")
          done <- struct{}{} // Сигнал главной горутине
      }()
      mustCopy(conn, os.Stdin)
      conn.Close()
      <-done // Ожидание завершения фоновой горутины
    }