N kanal nasıl dinlenir? (dinamik seçim ifadesi)


116

iki gorutini çalıştırmanın sonsuz döngüsünü başlatmak için aşağıdaki kodu kullanabilirim:

mesajı aldıktan sonra yeni bir gorutin başlatacak ve sonsuza kadar devam edecek.

c1 := make(chan string)
c2 := make(chan string)

go DoStuff(c1, 5)
go DoStuff(c2, 2)

for ; true;  {
    select {
    case msg1 := <-c1:
        fmt.Println("received ", msg1)
        go DoStuff(c1, 1)
    case msg2 := <-c2:
        fmt.Println("received ", msg2)
        go DoStuff(c2, 9)
    }
}

Şimdi N goroutin için aynı davranışı istiyorum, ancak bu durumda select ifadesi nasıl görünecek?

Bu, başladığım kod biti, ancak select ifadesini nasıl kodlayacağımı kafam karıştı

numChans := 2

//I keep the channels in this slice, and want to "loop" over them in the select statemnt
var chans = [] chan string{}

for i:=0;i<numChans;i++{
    tmp := make(chan string);
    chans = append(chans, tmp);
    go DoStuff(tmp, i + 1)

//How shall the select statment be coded for this case?  
for ; true;  {
    select {
    case msg1 := <-c1:
        fmt.Println("received ", msg1)
        go DoStuff(c1, 1)
    case msg2 := <-c2:
        fmt.Println("received ", msg2)
        go DoStuff(c2, 9)
    }
}

4
Sanırım istediğin Kanal Çoğullama. golang.org/doc/effective_go.html#chan_of_chan Temel olarak, dinlediğiniz tek bir kanalınız ve ardından ana kanala akan birden fazla alt kanalınız var. İlgili SO Soru: stackoverflow.com/questions/10979608/…
Brenden

Yanıtlar:


152

Bunu Select, yansıtma paketindeki işlevi kullanarak yapabilirsiniz :

func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)

Select, vaka listesi tarafından açıklanan bir seçme işlemini yürütür. Go select deyimi gibi, davalardan en az biri ilerleyene kadar bloke eder, tek tip bir sözde rastgele seçim yapar ve ardından bu durumu yürütür. Seçilen durumun indeksini döndürür ve eğer bu durum bir alma işlemi ise, alınan değer ve değerin kanaldaki bir gönderime karşılık gelip gelmediğini gösteren bir boole (kanal kapalı olduğu için alınan sıfır değerinin aksine).

SelectCaseSeçilecek kanalı, işlemin yönünü ve bir gönderme işlemi durumunda gönderilecek bir değeri tanımlayan bir yapı dizisi iletirsiniz .

Böylece şöyle bir şey yapabilirsiniz:

cases := make([]reflect.SelectCase, len(chans))
for i, ch := range chans {
    cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)}
}
chosen, value, ok := reflect.Select(cases)
// ok will be true if the channel has not been closed.
ch := chans[chosen]
msg := value.String()

Burada daha ayrıntılı bir örnekle deneyebilirsiniz: http://play.golang.org/p/8zwvSk4kjx


4
Bu tür seçimlerde dava sayısında pratik bir sınır var mı? Bunun ötesine geçerseniz, performans ciddi şekilde etkilenecek mi?
Maxim Vladimirsky

4
Belki bu benim yetersizliğimdir, ancak kanal aracılığıyla karmaşık yapılar gönderip alırken bu modelle çalışmanın gerçekten zor olduğunu gördüm. Tim Allclair'in dediği gibi, paylaşılan bir "toplu" kanalı geçirmek benim durumumda çok daha kolaydı.
Bora M. Alper

90

Bunu, her kanalı, mesajları paylaşılan bir "toplu" kanala "ileten" bir grup içine sararak başarabilirsiniz. Örneğin:

agg := make(chan string)
for _, ch := range chans {
  go func(c chan string) {
    for msg := range c {
      agg <- msg
    }
  }(ch)
}

select {
case msg <- agg:
    fmt.Println("received ", msg)
}

Mesajın hangi kanaldan geldiğini bilmeniz gerekiyorsa, mesajı toplu kanala iletmeden önce herhangi bir ekstra bilgiyle bir yapıya sarabilirsiniz.

(Sınırlı) testimde, bu yöntem yansıtma paketini kullanarak büyük ölçüde performans gösterir:

$ go test dynamic_select_test.go -test.bench=.
...
BenchmarkReflectSelect         1    5265109013 ns/op
BenchmarkGoSelect             20      81911344 ns/op
ok      command-line-arguments  9.463s

Karşılaştırma kodu burada


2
Karşılaştırma kodunuz yanlış, bir kıyaslama içinde döngüb.N yapmanız gerekiyor . Aksi takdirde sonuçlar ( b.Nçıktınızda 1 ve 2000000000'e bölünen) tamamen anlamsız olacaktır.
Dave C

2
@DaveC Teşekkürler! Sonuç değişmez, ancak sonuçlar çok daha mantıklıdır.
Tim Allclair

1
Aslında, bazı gerçek sayılar elde etmek için karşılaştırma kodunuzu hızlıca hackledim . Bu kıyaslamada hala eksik / yanlış bir şeyler olabilir, ancak daha karmaşık yansıtma kodunun başarabileceği tek şey, kurulumun daha hızlı olmasıdır (GOMAXPROCS = 1 ile) çünkü bir grup gorutine ihtiyaç duymaz. Diğer her durumda, basit bir gorutin birleştirme kanalı yansıtma çözümünü uçurur (~ 2 büyüklük sırası ile).
Dave C

2
( reflect.SelectYaklaşımla karşılaştırıldığında) önemli bir dezavantajı , birleştirilen her kanalda minimum tek bir değerde birleştirme tamponunu yapan grupların olmasıdır. Genellikle bu bir sorun olmaz, ancak bazı özel uygulamalarda anlaşmayı bozabilir :(.
Dave C

1
tamponlu bir birleştirme kanalı sorunu daha da kötüleştirir. Sorun, yalnızca yansıtma çözümünün tamamen arabelleğe alınmamış semantiğe sahip olabilmesidir. Devam ettim ve denediğim test kodunu ayrı bir cevap olarak (umarım) söylemeye çalıştığım şeyi açıklığa kavuşturmak için gönderdim.
Dave C

22

Önceki cevaplarla ilgili bazı yorumları genişletmek ve daha net bir karşılaştırma sağlamak için, burada şimdiye kadar aynı girdiye verilen her iki yaklaşımın bir örneği, okunacak bir kanal dilimi ve her bir değer için çağrılacak bir fonksiyonun hangisi olduğunu da bilmek gerekir. değerin geldiği kanal.

Yaklaşımlar arasında üç ana fark vardır:

  • Karmaşıklık. Kısmen bir okuyucu tercihi olsa da, kanal yaklaşımını daha deyimsel, açık ve okunaklı buluyorum.

  • Verim. Xeon amd64 sistemimde gorutinler + kanallar yansıtma çözümünü yaklaşık iki büyüklük düzeyinde gerçekleştirir (genel olarak Go'daki yansıma genellikle daha yavaştır ve yalnızca kesinlikle gerekli olduğunda kullanılmalıdır). Elbette, sonuçları işleyen fonksiyonda veya değerlerin giriş kanallarına yazılmasında önemli bir gecikme olursa, bu performans farkı kolayca önemsiz hale gelebilir.

  • Anlambelleği engelleme / arabelleğe alma. Bunun önemi, kullanım durumuna bağlıdır. Çoğu zaman ya önemli olmaz ya da gorutin birleştirme çözümündeki fazladan hafif bir tamponlama, iş hacmi için yardımcı olabilir. Bununla birlikte, yalnızca tek bir yazarın engelinin kaldırılması ve değerinin başka herhangi bir yazarın engeli kaldırılmadan önce tam olarak ele alınması anlamsallığına sahip olmak isteniyorsa, bu yalnızca yansıtma çözümü ile elde edilebilir.

Gönderen kanalın "kimliği" gerekmiyorsa veya kaynak kanalları hiçbir zaman kapatılmayacaksa, her iki yaklaşımın da basitleştirilebileceğini unutmayın.

Goroutine birleştirme kanalı:

// Process1 calls `fn` for each value received from any of the `chans`
// channels. The arguments to `fn` are the index of the channel the
// value came from and the string value. Process1 returns once all the
// channels are closed.
func Process1(chans []<-chan string, fn func(int, string)) {
    // Setup
    type item struct {
        int    // index of which channel this came from
        string // the actual string item
    }
    merged := make(chan item)
    var wg sync.WaitGroup
    wg.Add(len(chans))
    for i, c := range chans {
        go func(i int, c <-chan string) {
            // Reads and buffers a single item from `c` before
            // we even know if we can write to `merged`.
            //
            // Go doesn't provide a way to do something like:
            //     merged <- (<-c)
            // atomically, where we delay the read from `c`
            // until we can write to `merged`. The read from
            // `c` will always happen first (blocking as
            // required) and then we block on `merged` (with
            // either the above or the below syntax making
            // no difference).
            for s := range c {
                merged <- item{i, s}
            }
            // If/when this input channel is closed we just stop
            // writing to the merged channel and via the WaitGroup
            // let it be known there is one fewer channel active.
            wg.Done()
        }(i, c)
    }
    // One extra goroutine to watch for all the merging goroutines to
    // be finished and then close the merged channel.
    go func() {
        wg.Wait()
        close(merged)
    }()

    // "select-like" loop
    for i := range merged {
        // Process each value
        fn(i.int, i.string)
    }
}

Yansıma seçimi:

// Process2 is identical to Process1 except that it uses the reflect
// package to select and read from the input channels which guarantees
// there is only one value "in-flight" (i.e. when `fn` is called only
// a single send on a single channel will have succeeded, the rest will
// be blocked). It is approximately two orders of magnitude slower than
// Process1 (which is still insignificant if their is a significant
// delay between incoming values or if `fn` runs for a significant
// time).
func Process2(chans []<-chan string, fn func(int, string)) {
    // Setup
    cases := make([]reflect.SelectCase, len(chans))
    // `ids` maps the index within cases to the original `chans` index.
    ids := make([]int, len(chans))
    for i, c := range chans {
        cases[i] = reflect.SelectCase{
            Dir:  reflect.SelectRecv,
            Chan: reflect.ValueOf(c),
        }
        ids[i] = i
    }

    // Select loop
    for len(cases) > 0 {
        // A difference here from the merging goroutines is
        // that `v` is the only value "in-flight" that any of
        // the workers have sent. All other workers are blocked
        // trying to send the single value they have calculated
        // where-as the goroutine version reads/buffers a single
        // extra value from each worker.
        i, v, ok := reflect.Select(cases)
        if !ok {
            // Channel cases[i] has been closed, remove it
            // from our slice of cases and update our ids
            // mapping as well.
            cases = append(cases[:i], cases[i+1:]...)
            ids = append(ids[:i], ids[i+1:]...)
            continue
        }

        // Process each value
        fn(ids[i], v.String())
    }
}

[ Go oyun alanında tam kod .]


1
Ayrıca goroutinler + kanal çözümünün her şeyi yapamayacağını selectveya yaptığını reflect.Selectbelirtmek gerekir. Gorutinler, kanallardan her şeyi tüketene kadar dönmeye devam edecekler, bu nedenle Process1erken çıkmanın net bir yolu yok . Ayrıca, birden fazla okuyucunuz varsa, gorutinler her bir kanaldan bir öğeyi arabelleğe aldığından, sorun çıkma olasılığı da vardır select.
James Henstridge

@JamesHenstridge, durmakla ilgili ilk notun doğru değil. İşlem1'i, aynı İşlem2'yi durdurmak için yaptığınız şekilde durdurursanız; örneğin, gorutinlerin durması gerektiğinde kapatılan bir "durdurma" kanalı ekleyerek. İşlem1, şu anda kullanılan daha basit döngü yerine selectbir fordöngü içinde iki duruma ihtiyaç duyar for range. Process2'nin başka bir vakayı yerleştirmesi casesve bu değeri özel olarak ele alması gerekir i.
Dave C

Bu, erken durdurma durumunda kullanılmayacak kanallardan değerleri okumanız sorununu hala çözmez.
James Henstridge

0

Birisinin olayları gönderdiğini varsayarsak bu yaklaşım neden işe yaramaz?

func main() {
    numChans := 2
    var chans = []chan string{}

    for i := 0; i < numChans; i++ {
        tmp := make(chan string)
        chans = append(chans, tmp)
    }

    for true {
        for i, c := range chans {
            select {
            case x = <-c:
                fmt.Printf("received %d \n", i)
                go DoShit(x, i)
            default: continue
            }
        }
    }
}

8
Bu bir spin döngüsüdür. Bir giriş kanalının bir değere sahip olmasını beklerken bu, mevcut tüm CPU'yu tüketir. selectBirden fazla kanalda (bir defaultmadde olmadan ) bütün mesele, en az bir tanesi dönmeden hazır olana kadar verimli bir şekilde beklemesidir.
Dave C

0

Muhtemelen daha basit seçenek:

Bir kanal dizisine sahip olmak yerine, neden ayrı gruplarda çalıştırılan işlevlere bir parametre olarak sadece bir kanal geçirip ardından kanalı bir tüketici programında dinlemiyorsunuz?

Bu, dinleyicinizdeki tek bir kanalı seçmenize, basit bir seçim yapmanıza ve birden çok kanaldan mesajları toplamak için yeni grupların oluşturulmasını engellemenize olanak tanır.

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.