Для чего используются каналы?

Просматривая код Go, я обнаружил следующее:

  ch := make(chan int)

В онлайн-учебнике я посмотрел, как работают каналы Go:

https://tour.golang.org/concurrency/2

Но я нахожу этот пример неясным.

Может кто-нибудь дать мне простое объяснение и пример использования каналов?

Ответ 1

Чан является каналом в Голанге. Проще говоря, вы можете думать об этом как о коробке, в которую вы кладете предмет с одного конца, а затем выбираете его с другого конца.

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

enter image description here

Буферизованный канал

enter image description here

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

    package main

    import (
        "fmt"
        "time"
    )

    func main() {
        messages := make(chan int)
        go func() {
            time.Sleep(time.Second * 3)
            messages <- 1
        }()
        go func() {
            time.Sleep(time.Second * 2)
            messages <- 2
        }() 
        go func() {
            time.Sleep(time.Second * 1)
            messages <- 3
        }()
        go func() {
            for i := range messages {
                fmt.Println(i)
            }
        }()
        go func() {
            time.Sleep(time.Second * 1)
            messages <- 4
        }()
        go func() {
            time.Sleep(time.Second * 1)
            messages <- 5
        }()
        time.Sleep(time.Second * 5)
    }

Для лучшего понимания посетите этот блог, где описаны процедуры и каналы в графическом интерфейсе.

Посещение http://divan.github.io/posts/go_concurrency_visualize/

Ответ 2

Я думаю, что спецификации довольно ясно об этом. Spec: типы каналов:

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

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

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

Вместо этого Go предпочитает каналы. Цитата из Effective Go: поделитесь, общаясь:

Не общайтесь, разделяя память; вместо этого делитесь памятью, общаясь.

Таким образом, вместо того, чтобы помещать сообщения в общий слайс, например, вы можете создать канал (видимый для обеих подпрограмм), и без какой-либо внешней синхронизации/блокировки, одна подпрограмма может отправлять сообщения (значения) через канал, а другая подпрограмма может получать им.

Только одна программа имеет доступ к значению в любой момент времени. Гонки данных не могут быть предусмотрены.

Таким образом, на самом деле любое количество подпрограмм может отправлять значения по одному и тому же каналу, и любое количество подпрограмм может получать значения из него, без какой-либо дальнейшей синхронизации. См. связанный вопрос для получения более подробной информации: Если я правильно использую каналы, нужно ли мне использовать мьютексы?

Пример канала

Давайте посмотрим на пример, где мы запускаем 2 дополнительные программы для одновременных вычислений. Мы передаем число первому, которое добавляет к нему 1 и доставляет результат по 2-му каналу. Вторая программа получит число, умножит его на 10 и доставит в канал результатов:

func AddOne(ch chan<- int, i int) {
    i++
    ch <- i
}

func MulBy10(ch <-chan int, resch chan<- int) {
    i := <-ch
    i *= 10
    resch <- i
}

Вот как это можно назвать/использовать:

func main() {
    ch := make(chan int)
    resch := make(chan int)

    go AddOne(ch, 9)
    go MulBy10(ch, resch)

    result := <-resch
    fmt.Println("Result:", result)
}

Общение по каналам также заботится о горутинах, ожидающих друг друга. В этом примере это означает, что MulBy10() будет ждать, пока AddOne() доставит увеличенное число, а main() будет ждать MulBy10() перед печатью результата. Вывод, как ожидается (попробуйте на Go Playground):

Result: 100

Языковая поддержка

Существует несколько языковых конструкций, предназначенных для удобного использования каналов, например:

  • for ... range на канале перебирает значения, полученные от канала, до тех пор, пока канал не будет закрыт.
  • Оператор select может использоваться для перечисления нескольких операций канала, таких как отправка по каналу и прием из канала, и будет выбрана та, которая может продолжаться без блокировки (случайным образом, если есть несколько операций, которые может продолжить и заблокирует, если ни один не готов).
  • Существует специальная форма оператора получения, которая позволяет вам проверить, был ли канал закрыт (помимо получения значения): v, ok := <-ch
  • Встроенная функция len() сообщает количество элементов в очереди (непрочитано); строительная cap() функция возвращает емкость буфера канала.

Другое использование

Для более практического примера посмотрите, как можно использовать каналы для реализации рабочего пула. Аналогичным образом используется распределение ценностей от производителя к потребителю (ям).

Другой практический пример - реализация пула памяти с использованием буферизованных каналов.

И еще один практический пример - элегантная реализация брокера.

Канал часто используется для тайм-аута некоторой операции блокировки, используя канал, возвращаемый time.After(), который "срабатывает" после указанной задержки/длительности ("срабатывает" означает, что на него будет отправлено значение). Посмотрите этот пример для демонстрации (попробуйте на Go Playground):

ch := make(chan int)

select {
case i := <-ch:
    fmt.Println("Received:", i)
case <-time.After(time.Second):
    fmt.Println("Timeout, no value received")
}

Его можно использовать для ожидания максимального количества времени для некоторого значения, но если другие процедуры не могут предоставить значение к этому времени, мы можем вместо этого решить сделать что-то еще.

Также особой формой общения может быть просто сигнализировать о завершении какой-либо операции (без фактической отправки каких-либо "полезных" данных). Такой случай может быть реализован каналом с любым типом элемента, например, chan int и отправив на него любое значение, например, 0. Но так как отправленное значение не содержит информации, вы можете объявить его как chan struct{}. Или, что еще лучше, если вам нужна только однократная сигнализация, вы можете просто закрыть канал, который можно перехватить на другой стороне, используя for ... range, или получить от него (так как прием с закрытого канала происходит немедленно, давая нулевое значение типа элемента). Также знайте, что, хотя канал может использоваться для этого вида сигнализации, есть лучшая альтернатива для этого: sync.WaitGroup.

Дальнейшее чтение

Чтобы избежать неожиданного поведения, стоит знать об аксиомах канала: Как ведет себя неинициализированный канал?

Блог Go: делитесь памятью, общаясь

Блог Go: шаблоны параллелизма Go: конвейеры и отмена

Блог Go: расширенные шаблоны параллелизма Go

Ardan labs: природа каналов в движении