Несколько горутин прослушивают один канал

84

У меня есть несколько горутин, которые пытаются получить одновременно на одном канале. Похоже, что последняя горутина, которая начинает получать на канале, получает значение. Это где-то в спецификации языка или это неопределенное поведение?

c := make(chan string)
for i := 0; i < 5; i++ {
    go func(i int) {
        <-c
        c <- fmt.Sprintf("goroutine %d", i)
    }(i)
}
c <- "hi"
fmt.Println(<-c)

Вывод:

goroutine 4

Пример на детской площадке

РЕДАКТИРОВАТЬ:

Я просто понял, что это сложнее, чем я думал. Сообщение передается по всем горутинам.

c := make(chan string)
for i := 0; i < 5; i++ {
    go func(i int) {
        msg := <-c
        c <- fmt.Sprintf("%s, hi from %d", msg, i)
    }(i)
}
c <- "original"
fmt.Println(<-c)

Вывод:

original, hi from 0, hi from 1, hi from 2, hi from 3, hi from 4

Пример на детской площадке

Илья Чоли
источник
6
Я попробовал ваш последний фрагмент, и (к моему огромному облегчению) он вывел только original, hi from 4...
Чан Цянь
1
@ChangQian добавление a time.Sleep(time.Millisecond)между отправкой и получением канала возвращает старое поведение.
Илья Чоли

Ответы:

78

Да, это сложно, но есть пара практических правил, которые должны сделать все намного проще.

  • предпочитают использовать формальные аргументы для каналов, которые вы передаете подпрограммам, вместо доступа к каналам в глобальной области. Таким образом вы можете получить больше проверок компилятора, а также лучшую модульность.
  • Избегайте чтения и записи на одном и том же канале в конкретном рутине (включая «основной»). В противном случае тупиковая ситуация представляет собой гораздо больший риск.

Вот альтернативная версия вашей программы, в которой применяются эти два правила. Этот случай демонстрирует, как много писателей и один читатель на канале:

c := make(chan string)

for i := 1; i <= 5; i++ {
    go func(i int, co chan<- string) {
        for j := 1; j <= 5; j++ {
            co <- fmt.Sprintf("hi from %d.%d", i, j)
        }
    }(i, c)
}

for i := 1; i <= 25; i++ {
    fmt.Println(<-c)
}

http://play.golang.org/p/quQn7xePLw

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

Этот пример демонстрирует особенность каналов Go: можно иметь несколько писателей, совместно использующих один канал; Go автоматически чередует сообщения.

То же самое относится к одному писателю и нескольким читателям на одном канале, как показано во втором примере здесь:

c := make(chan int)
var w sync.WaitGroup
w.Add(5)

for i := 1; i <= 5; i++ {
    go func(i int, ci <-chan int) {
        j := 1
        for v := range ci {
            time.Sleep(time.Millisecond)
            fmt.Printf("%d.%d got %d\n", i, j, v)
            j += 1
        }
        w.Done()
    }(i, c)
}

for i := 1; i <= 25; i++ {
    c <- i
}
close(c)
w.Wait()

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

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

Рик-777
источник
разве вам не нужно ждать завершения всех горутин?
mlbright
Все зависит от того, что вы имеете в виду. Взгляните на примеры play.golang.org; у них есть mainфункция, которая завершается, как только достигает конца, независимо от того, что делают другие горутины. В первом примере выше mainэто синхронизированный шаг с другими горутинами, поэтому проблем нет. Второй пример также работает без проблем , поскольку все сообщения отправляются через c доclose того вызывается функция , и это происходит прежде , чем в maingoroutine завершается. (Вы можете возразить, что звонки closeв данном случае излишни, но это хорошая практика.)
Rick-777
1
предполагая, что вы хотите (детерминированно) увидеть 15 распечаток в последнем примере, вам нужно подождать. Чтобы продемонстрировать это, вот тот же пример, но со временем. Сон перед Printf
olov
А вот тот же пример с time.Sleep и исправлен с помощью WaitGroup для ожидания горутин
olov
Я не думаю, что это хорошая рекомендация сначала отказаться от буферизации. Без буферизации вы фактически не пишете параллельный код, и это приводит не только к невозможности взаимоблокировки, но и к тому, что результат обработки с другой стороны канала уже доступен в следующей инструкции после отправки, и вы можете непреднамеренно (или намеренно в случае новичка) полагаться на это. И как только вы полагаетесь на тот факт, что вы получаете результат немедленно, без особого ожидания его, и добавляете буфер, у вас возникает состояние гонки.
пользователь
25

Поздний ответ, но я надеюсь, что это поможет другим в будущем, например, Длинный опрос, «Глобальная» кнопка, Трансляция для всех?

Effective Go объясняет проблему:

Получатели всегда блокируются до тех пор, пока не появятся данные для приема.

Это означает, что у вас не может быть более 1 горутины, слушающей 1 канал, и ожидать, что ВСЕ горутины получат одинаковое значение.

Запустите этот пример кода .

package main

import "fmt"

func main() {
    c := make(chan int)

    for i := 1; i <= 5; i++ {
        go func(i int) {
        for v := range c {
                fmt.Printf("count %d from goroutine #%d\n", v, i)
            }
        }(i)
    }

    for i := 1; i <= 25; i++ {
        c<-i
    }

    close(c)
}

Вы не увидите "count 1" более одного раза, даже если есть 5 горутин, прослушивающих канал. Это потому, что когда первая горутина блокирует канал, все остальные горутины должны ждать в очереди. Когда канал разблокирован, счет уже был получен и удален из канала, поэтому следующая горутина в строке получит следующее значение счетчика.

Brenden
источник
1
Спасибо - теперь этот пример имеет смысл github.com/goinaction/code/blob/master/chapter6/listing20/…
user31208 09
Ах, это было полезно. Может ли быть хорошей альтернативой создание канала для каждой процедуры Go, которая нуждается в информации, а затем при необходимости отправлять сообщение по всем каналам? Я могу себе представить такой вариант.
ThePartyTurtle,
9

Это сложно.

Также посмотрите, что происходит с GOMAXPROCS = NumCPU+1. Например,

package main

import (
    "fmt"
    "runtime"
)

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU() + 1)
    fmt.Print(runtime.GOMAXPROCS(0))
    c := make(chan string)
    for i := 0; i < 5; i++ {
        go func(i int) {
            msg := <-c
            c <- fmt.Sprintf("%s, hi from %d", msg, i)
        }(i)
    }
    c <- ", original"
    fmt.Println(<-c)
}

Вывод:

5, original, hi from 4

И посмотрите, что происходит с буферизованными каналами. Например,

package main

import "fmt"

func main() {
    c := make(chan string, 5+1)
    for i := 0; i < 5; i++ {
        go func(i int) {
            msg := <-c
            c <- fmt.Sprintf("%s, hi from %d", msg, i)
        }(i)
    }
    c <- "original"
    fmt.Println(<-c)
}

Вывод:

original

Вы тоже должны уметь объяснять эти случаи.

Питер
источник
7

Я изучил существующие решения и создал простую широковещательную библиотеку https://github.com/grafov/bcast .

    group := bcast.NewGroup() // you created the broadcast group
    go bcast.Broadcasting(0) // the group accepts messages and broadcast it to all members

    member := group.Join() // then you join member(s) from other goroutine(s)
    member.Send("test message") // or send messages of any type to the group 

    member1 := group.Join() // then you join member(s) from other goroutine(s)
    val := member1.Recv() // and for example listen for messages
Графов Александр Иванович
источник
2
Отличная библиотека у вас там! Я нашел также github.com/asaskevich/EventBus
пользователь
И это не имеет большого значения, но, возможно, вам следует упомянуть, как отключиться, в ридми.
пользователь
Утечка памяти там
jhvaras 03
:( Не могли бы вы объяснить подробности @jhvaras?
Александр Иванович Графов 08
2

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

package main

import (
    "fmt"
    "sync"
)

type obj struct {
    msg string
    receiver int
}

func main() {
    ch := make(chan *obj) // both block or non-block are ok
    var wg sync.WaitGroup
    receiver := 25 // specify receiver count

    sender := func() {
        o := &obj {
            msg: "hello everyone!",
            receiver: receiver,
        }
        ch <- o
    }
    recv := func(idx int) {
        defer wg.Done()
        o := <-ch
        fmt.Printf("%d received at %d\n", idx, o.receiver)
        o.receiver--
        if o.receiver > 0 {
            ch <- o // forward to others
        } else {
            fmt.Printf("last receiver: %d\n", idx)
        }
    }

    go sender()
    for i:=0; i<reciever; i++ {
        wg.Add(1)
        go recv(i)
    }

    wg.Wait()
}

Результат случайный:

5 received at 25
24 received at 24
6 received at 23
7 received at 22
8 received at 21
9 received at 20
10 received at 19
11 received at 18
12 received at 17
13 received at 16
14 received at 15
15 received at 14
16 received at 13
17 received at 12
18 received at 11
19 received at 10
20 received at 9
21 received at 8
22 received at 7
23 received at 6
2 received at 5
0 received at 4
1 received at 3
3 received at 2
4 received at 1
last receiver 4
коанор
источник