Кілька програмних програм, що слухають на одному каналі


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 додавання time.Sleep(time.Millisecond)між каналом надсилання та отримання повертає стару поведінку.
Ilia Choly

Відповіді:


78

Так, це складно, але є кілька ескізних правил, які повинні зробити речі набагато простішими.

  • віддайте перевагу використанню формальних аргументів для каналів, які ви передаєте go-процедурам, замість доступу до каналів у глобальній області. Ви можете отримати більше компілятора, перевіряючи таким чином, і кращу модульність.
  • уникайте як читання, так і письма на одному каналі в певній рутинній програмі (включаючи «основну»). В іншому випадку глухий кут є набагато більшим ризиком.

Ось альтернативна версія вашої програми із застосуванням цих двох рекомендацій. Цей випадок демонструє багато авторів та одного читача на каналі:

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 за цю корекцію) .

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


чи не потрібно чекати, поки закінчаться всі програми?
mlbright

Це залежить від того, що ви маєте на увазі. Погляньте на приклади play.golang.org; вони мають mainфункцію, яка припиняється, коли вона досягає кінця, незалежно від того, що роблять будь-які інші програми. У першому прикладі, наведеному вище, mainє блокування з іншими програмами, тому немає проблем. Другий приклад також працює без проблем , оскільки всі повідомлення відправляються через c доclose того викликається функція , і це відбувається перш , ніж в maingoroutine завершується. (Ви можете заперечити, що дзвінки closeв цьому випадку зайві, але це хороша практика.)
Rick-777,

1
припускаючи, що ви хочете (детерміновано) побачити 15 роздруківки в останньому прикладі, вам потрібно почекати. Щоб продемонструвати це, ось той самий приклад, але з часом. Сон безпосередньо перед Printf: play.golang.org/p/cEP-UBPLv6
olov

І ось той самий приклад з часом. Сон і виправлений за допомогою 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)
}

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


1
Дякую - тепер цей приклад має сенс github.com/goinaction/code/blob/master/chapter6/listing20/…
user31208

Ааа, це було корисно. Чи хорошою альтернативою було б створити канал для кожної програми 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
користувача

І не велика справа, але, можливо, вам слід згадати, як від’єднатися від readme.
користувач

Витік пам'яті там
джварас

:( Чи можете ви пояснити подробиці @jhvaras?
Олександр Іванович Графов

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.