Tek kanalda dinleyen birden fazla gorutin


84

Aynı kanalda aynı anda almaya çalışan birden çok gorutin var. Kanalda almaya başlayan son gorutin değeri alıyor gibi görünüyor. Bu dil spesifikasyonunda bir yerde mi yoksa tanımlanmamış bir davranış mı?

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)

Çıktı:

goroutine 4

Oyun Alanında Örnek

DÜZENLE:

Düşündüğümden daha karmaşık olduğunu anladım. Mesaj tüm gorutinlerin etrafından geçer.

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)

Çıktı:

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

Oyun Alanında Örnek


6
Son pasajınızı denedim ve (büyük rahatlamam için) sadece çıktı original, hi from 4...
Chang Qian

1
@ChangQian time.Sleep(time.Millisecond)gönderme ve alma kanalları arasına bir eklemek eski davranışı geri getirir.
Ilia Choly

Yanıtlar:


78

Evet, karmaşık ama işleri daha kolay hale getirecek birkaç temel kural var.

  • Global kapsamdaki kanallara erişmek yerine, rutinlere ilettiğiniz kanallar için resmi argümanlar kullanmayı tercih edin . Bu şekilde daha fazla derleyici denetimi ve daha iyi modülerlik elde edebilirsiniz.
  • aynı kanalda ('ana' olan dahil) hem okumaktan hem de yazmaktan kaçının . Aksi takdirde, kilitlenme çok daha büyük bir risktir.

İşte programınızın bu iki yönergeyi uygulayan alternatif bir sürümü. Bu vaka, bir kanaldaki birçok yazarı ve bir okuyucuyu göstermektedir:

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

Her biri beş kez yazan tek bir kanala yazan beş go-rutini oluşturur. Ana go-rutini yirmi beş mesajın hepsini okur - göründükleri sıranın genellikle sıralı olmadığını fark edebilirsiniz (yani eşzamanlılık açıktır).

Bu örnek, Go kanallarının bir özelliğini göstermektedir: bir kanalı paylaşan birden çok yazarın olması mümkündür; Git, mesajları otomatik olarak araya ekleyecektir.

Aynısı, buradaki ikinci örnekte görüldüğü gibi, bir kanaldaki bir yazar ve birden fazla okuyucu için de geçerlidir:

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

Bu ikinci örnek , ana gorutine uygulanan ve diğer beş gorutinin erken sonlandırılmasına neden olan ( bu düzeltme için olov sayesinde) bir beklemeyi içerir .

Her iki örnekte de arabelleğe almaya gerek yoktu. Arabelleğe almayı yalnızca bir performans artırıcı olarak görmek genellikle iyi bir ilkedir. Programınız tamponlar olmadan kilitlenmezse , tamponlarla da kilitlenmez (ancak tersi her zaman doğru değildir ). Bu nedenle, başka bir pratik kural olarak, arabelleğe almadan başlayın ve daha sonra gerektiği gibi ekleyin .


tüm gorutinlerin bitmesini beklemenize gerek yok mu?
mlbright

Ne demek istediğine bağlı. Play.golang.org örneklerine bir göz atın; maindiğer gorutinlerin ne yaptığına bakılmaksızın, sona ulaştığında sona eren bir işlevi vardır. Yukarıdaki ilk örnekte main, diğer gorutinler ile kilit adımı olduğu için sorun yok. Tüm mesajlar aracılığıyla gönderilir çünkü ikinci örnek de sorunsuz çalışır c önceclose fonksiyon denir ve bu durumda öncemain goroutine sonlandığı. (Bu closedurumda aramanın gereksiz olduğunu iddia edebilirsiniz , ancak bu iyi bir uygulamadır.)
Rick-777

1
Son örnekte (belirleyici olarak) 15 çıktı görmek istediğinizi varsayarsak, beklemeniz gerekir. Bunu göstermek için, işte aynı örnek, ancak bir zamanla. Printf'den hemen önce uyku
olov

Ve işte aynı örnek bir time.Sleep ve bir WaitGroup ile goroutinleri beklemek için sabitleyin: play.golang.org/p/ESq9he_WzS
olov

İlk başta arabelleğe almayı ihmal etmek için iyi bir öneri olduğunu sanmıyorum. Arabelleğe almadan aslında eşzamanlı kod yazmazsınız ve bu sadece kilitlenememenize değil, aynı zamanda kanalın diğer tarafındaki işleme sonucunun göndermeden sonraki bir sonraki talimatta zaten mevcut olmasına yol açar ve istemeden (veya bir acemi durumunda kasıtlı olarak olay) buna güvenebilirsiniz. Ve özel olarak beklemeden hemen bir sonuca sahip olduğunuza güvendiğinizde ve bir tampon eklediğinizde, bir yarış durumunuz olur.
kullanıcı

25

Geç cevap, ama umarım bu gelecekte Long Polling, "Global" Button, Herkese yayınla?

Effective Go sorunu şöyle açıklıyor:

Alıcılar, alınacak veri olana kadar her zaman bloke eder.

Bu, 1 kanalı dinleyen 1'den fazla gorutine sahip olamayacağınız ve TÜM gorutinlerin aynı değeri almasını bekleyemeyeceğiniz anlamına gelir.

Bu Kod Örneğini çalıştırın .

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

Kanalı dinleyen 5 gorutin olmasına rağmen "1'i" birden fazla görmeyeceksiniz. Bunun nedeni, ilk grup kanalı engellediğinde diğer tüm gorutinlerin sırada beklemesi gerektiğidir. Kanalın engeli kaldırıldığında, sayım zaten alınmış ve kanaldan kaldırılmıştır, böylece sıradaki bir sonraki gorutin bir sonraki sayım değerini alır.


1
Teşekkürler - şimdi bu örnek mantıklı github.com/goinaction/code/blob/master/chapter6/listing20/…
user31208

Ahh bu yardımcı oldu. Bilgiye ihtiyaç duyan her Go rutini için bir kanal oluşturmak ve ardından gerektiğinde tüm kanallara mesaj göndermek iyi bir alternatif olabilir mi? Hayal edebileceğim seçenek bu.
ThePartyTurtle

9

Karmaşık bir durum.

Ayrıca, ne olacağını görün GOMAXPROCS = NumCPU+1. Örneğin,

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

Çıktı:

5, original, hi from 4

Ve arabelleğe alınmış kanallarda ne olduğunu görün. Örneğin,

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

Çıktı:

original

Bu vakaları da açıklayabilmelisiniz.


7

Mevcut çözümleri inceledim ve basit bir yayın kütüphanesi oluşturdum 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
Orada harika bir lib var! Ben de bulduk github.com/asaskevich/EventBus
kullanıcı

Ve önemli bir şey değil, ama benioku programına nasıl katılacağınızı belirtmelisiniz.
kullanıcı

Orada bellek sızıntısı
jhvaras

:( Ayrıntıları açıklayabilir misin @jhvaras?
Alexander I. Grafov

2

Bir kanalda birden fazla gorutin dinlemek için evet, mümkün. kilit nokta mesajın kendisidir, şöyle bir mesaj tanımlayabilirsiniz:

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

Çıktı rastgele:

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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.