Nhiều goroutines lắng nghe trên một kênh


82

Tôi có nhiều goroutines đang cố gắng nhận đồng thời trên cùng một kênh. Có vẻ như quy trình cuối cùng bắt đầu nhận trên kênh sẽ nhận được giá trị. Đây có phải là một nơi nào đó trong thông số ngôn ngữ hay là hành vi không xác định?

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)

Đầu ra:

goroutine 4

Ví dụ trên sân chơi

BIÊN TẬP:

Tôi chỉ nhận ra rằng nó phức tạp hơn tôi nghĩ. Thông điệp được chuyển xung quanh tất cả các goroutines.

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)

Đầu ra:

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

Ví dụ trên sân chơi


6
Tôi đã thử đoạn mã cuối cùng của bạn và (với sự cứu trợ rất lớn của tôi) nó chỉ xuất ra original, hi from 4...
Chang Qian

1
@ChangQian thêm một time.Sleep(time.Millisecond)giữa kênh gửi và nhận sẽ mang lại hành vi cũ.
Ilia Choly

Câu trả lời:


75

Vâng, nó phức tạp, nhưng có một vài quy tắc chung sẽ khiến mọi thứ trở nên đơn giản hơn nhiều.

  • thích sử dụng các đối số chính thức cho các kênh bạn chuyển vào quy trình thay vì truy cập các kênh trong phạm vi toàn cầu. Bạn có thể kiểm tra trình biên dịch nhiều hơn theo cách này và mô đun tốt hơn.
  • tránh cả việc đọc và viết trên cùng một kênh trong một quy trình cụ thể (bao gồm cả kênh 'chính'). Nếu không, bế tắc là một rủi ro lớn hơn nhiều.

Đây là phiên bản thay thế của chương trình của bạn, áp dụng hai nguyên tắc này. Trường hợp này chứng tỏ nhiều người viết và một người đọc trên một kênh:

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

Nó tạo ra năm quy trình viết vào một kênh duy nhất, mỗi quy trình viết năm lần. Quy trình chính đọc tất cả 25 thông báo - bạn có thể nhận thấy rằng thứ tự chúng xuất hiện thường không theo trình tự (tức là sự đồng thời là hiển nhiên).

Ví dụ này cho thấy một đặc điểm của kênh cờ vây: có thể có nhiều người viết chia sẻ một kênh; Go sẽ tự động xen kẽ các tin nhắn.

Điều tương tự cũng áp dụng cho một người viết và nhiều người đọc trên một kênh, như được thấy trong ví dụ thứ hai ở đây:

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()

Ví dụ thứ hai này bao gồm một sự chờ đợi áp đặt đối với quy trình chính, nếu không thì quy trình này sẽ thoát ra ngoài kịp thời và khiến năm tuyến gorout khác bị kết thúc sớm (nhờ olov cho sự điều chỉnh này) .

Trong cả hai ví dụ, không cần bộ đệm. Nói chung, đó là một nguyên tắc tốt để xem bộ đệm chỉ như một công cụ nâng cao hiệu suất. Nếu chương trình của bạn không deadlock mà không có bộ đệm, thì nó cũng sẽ không bị deadlock với bộ đệm (nhưng điều ngược lại không phải lúc nào cũng đúng). Vì vậy, như một quy tắc chung khác, hãy bắt đầu mà không cần lưu vào bộ đệm, sau đó thêm nó vào sau khi cần .


bạn không cần phải đợi cho tất cả các goroutines hoàn thành?
mlbright

Nó phụ thuộc vào ý bạn. Hãy xem các ví dụ play.golang.org; chúng có một mainchức năng kết thúc khi nó kết thúc, bất kể những gì các goroutines khác đang làm. Trong ví dụ đầu tiên ở trên, mainlà khóa bước với các goroutines khác nên không có vấn đề gì. Ví dụ thứ hai cũng làm việc mà không có vấn đề bởi vì tất cả các thông điệp được gửi qua c trước khi các closehàm được gọi và điều này xảy ra trước khi các mainchấm dứt goroutine. (Bạn có thể cho rằng gọi closelà thừa trong trường hợp, nhưng đó là thực hành tốt.)
Rick-777

1
giả sử rằng bạn muốn (một cách xác định) xem 15 bản in trong ví dụ cuối cùng, bạn cần phải đợi. Để chứng minh rằng, đây là ví dụ tương tự nhưng với một time.Sleep ngay trước khi printf: play.golang.org/p/cEP-UBPLv6
Olov

Và đây là những ví dụ tương tự với một time.Sleep và cố định với một WaitGroup để chờ cho goroutines: play.golang.org/p/ESq9he_WzS
Olov

Tôi không nghĩ rằng đó là một khuyến nghị tốt để bỏ qua bộ đệm lúc đầu. Nếu không có bộ đệm, bạn thực sự không viết mã đồng thời, và điều đó không chỉ dẫn đến việc bạn không thể bế tắc, mà còn dẫn đến kết quả xử lý từ phía bên kia của kênh đã có sẵn trong hướng dẫn tiếp theo sau khi gửi và bạn có thể vô tình (hoặc sự kiện cố ý trong trường hợp là người mới) dựa vào đó. Và một khi bạn dựa vào thực tế là bạn có kết quả ngay lập tức, không cần chờ đợi đặc biệt, và bạn thêm bộ đệm, bạn có một điều kiện đua.
người dùng

24

Trả lời muộn, nhưng tôi hy vọng điều này sẽ giúp ích cho những người khác trong tương lai như Long Polling, "Global" Button, Broadcast tới mọi người?

Go hiệu quả giải thích vấn đề:

Người nhận luôn chặn cho đến khi có dữ liệu để nhận.

Điều đó có nghĩa là bạn không thể có nhiều hơn 1 quy trình nghe trên 1 kênh và mong đợi TẤT CẢ các quy trình tuyến tính đều nhận được cùng một giá trị.

Chạy ví dụ mã này .

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)
}

Bạn sẽ không thấy "đếm 1" nhiều hơn một lần mặc dù có 5 tuyến sinh dục đang nghe kênh. Điều này là do khi quy trình đầu tiên chặn kênh, tất cả các quy trình khác phải xếp hàng chờ. Khi kênh được bỏ chặn, số lượng đã được nhận và bị xóa khỏi kênh, do đó, quy trình tiếp theo trong dòng nhận giá trị số lượng tiếp theo.


1
Cảm ơn - bây giờ ví dụ này có ý nghĩa github.com/goinaction/code/blob/master/chapter6/listing20/...
user31208

Ahh điều này rất hữu ích. Một giải pháp thay thế tốt có phải là tạo một kênh cho mỗi quy trình Go cần thông tin, sau đó gửi tin nhắn trên tất cả các kênh khi cần thiết không? Đó là lựa chọn tôi có thể tưởng tượng.
ThePartyTurtle

8

Nó là phức tạp.

Ngoài ra, hãy xem điều gì xảy ra với GOMAXPROCS = NumCPU+1. Ví dụ,

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)
}

Đầu ra:

5, original, hi from 4

Và, hãy xem điều gì xảy ra với các kênh được đệm. Ví dụ,

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)
}

Đầu ra:

original

Bạn cũng có thể giải thích những trường hợp này.


7

Tôi đã nghiên cứu các giải pháp hiện có và tạo thư viện quảng bá đơn giản 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
Tuyệt vời bạn có ở đó! Tôi cũng đã tìm thấy github.com/asaskevich/EventBus
người dùng

Và không phải là vấn đề lớn, nhưng có lẽ bạn nên đề cập đến cách bỏ tham gia readme.
người dùng

Rò rỉ bộ nhớ ở đó
jhvaras

:( bạn có thể giải thích chi tiết @jhvaras?
Alexander I.Grafov

2

Đối với nhiều quy trình nghe trên một kênh, có, hoàn toàn có thể. điểm mấu chốt là bản thân thông điệp, bạn có thể xác định một số thông báo như thế:

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()
}

Đầu ra là ngẫu nhiên:

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.