วิธีฟัง N ช่อง? (คำสั่งเลือกแบบไดนามิก)


116

เพื่อเริ่มต้นการวนซ้ำที่ไม่มีที่สิ้นสุดของการดำเนินการสอง goroutines ฉันสามารถใช้รหัสด้านล่าง:

หลังจากได้รับข้อความแล้วมันจะเริ่ม goroutine ใหม่และดำเนินต่อไปตลอดกาล

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

ตอนนี้ฉันต้องการมีพฤติกรรมเหมือนกันสำหรับ N goroutines แต่คำสั่ง select จะมีลักษณะอย่างไรในกรณีนั้น?

นี่คือรหัสบิตที่ฉันเริ่มต้นด้วย แต่ฉันสับสนว่าจะเขียนโค้ดคำสั่ง select อย่างไร

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
ฉันคิดว่าสิ่งที่คุณต้องการคือ Channel Multiplexing golang.org/doc/effective_go.html#chan_of_chan โดยทั่วไปคุณมีช่องเดียวที่คุณฟังและช่องย่อยหลายช่องที่เชื่อมต่อไปยังช่องหลัก คำถามที่เกี่ยวข้อง: stackoverflow.com/questions/10979608/…
Brenden

คำตอบ:


152

คุณสามารถทำได้โดยใช้Selectฟังก์ชันจากแพ็คเกจสะท้อน :

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

เลือกดำเนินการดำเนินการเลือกที่อธิบายโดยรายการกรณีและปัญหา เช่นเดียวกับคำสั่ง Go select จะบล็อกจนกว่าอย่างน้อยหนึ่งกรณีจะสามารถดำเนินการต่อได้ทำการเลือกแบบสุ่มหลอกที่เหมือนกันจากนั้นดำเนินการกรณีนั้น ส่งคืนดัชนีของกรณีที่เลือกและหากกรณีนั้นเป็นการดำเนินการรับค่าที่ได้รับและบูลีนที่ระบุว่าค่านั้นสอดคล้องกับการส่งบนช่องสัญญาณหรือไม่ (ตรงข้ามกับค่าศูนย์ที่ได้รับเนื่องจากช่องถูกปิด)

คุณส่งผ่านอาร์เรย์ของSelectCaseโครงสร้างที่ระบุช่องทางที่จะเลือกทิศทางของการดำเนินการและค่าที่จะส่งในกรณีของการดำเนินการส่ง

คุณสามารถทำสิ่งนี้ได้:

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

คุณสามารถทดลองกับตัวอย่างเพิ่มเติมได้ที่นี่: http://play.golang.org/p/8zwvSk4kjx


4
มีการ จำกัด จำนวนกรณีในทางปฏิบัติหรือไม่? สิ่งที่ถ้าคุณไปไกลกว่านั้นประสิทธิภาพจะได้รับผลกระทบอย่างรุนแรง?
Maxim Vladimirsky

4
อาจจะเป็นความไร้ความสามารถของฉัน แต่ฉันพบว่ารูปแบบนี้ยากที่จะทำงานด้วยเมื่อคุณส่งและรับโครงสร้างที่ซับซ้อนผ่านช่องทาง การส่งผ่านช่อง "รวม" ที่แชร์ตามที่ Tim Allclair กล่าวนั้นง่ายกว่ามากในกรณีของฉัน
Bora M. Alper

90

คุณสามารถทำได้โดยการรวมแต่ละช่องด้วย goroutine ซึ่ง "ส่งต่อ" ข้อความไปยังช่อง "รวม" ที่แชร์ ตัวอย่างเช่น:

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

หากคุณต้องการทราบว่าข้อความมาจากแชแนลใดคุณสามารถรวมข้อความนั้นไว้ในโครงสร้างด้วยข้อมูลเพิ่มเติมก่อนที่จะส่งต่อไปยังแชนเนลรวม

ในการทดสอบ (แบบ จำกัด ) ของฉันวิธีนี้ใช้งานได้ดีมากโดยใช้แพ็คเกจสะท้อนแสง:

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

Benchmark code ที่นี่


2
รหัสมาตรฐานของคุณไม่ถูกต้องคุณต้องวนซ้ำb.Nภายในเกณฑ์มาตรฐาน มิฉะนั้นผลลัพธ์ (ซึ่งหารด้วยb.N1 และ 2000000000 ในผลลัพธ์ของคุณ) จะไม่มีความหมายโดยสิ้นเชิง
Dave C

2
@DaveC ขอบคุณ! ข้อสรุปไม่เปลี่ยนแปลง แต่ผลลัพธ์มีเหตุผลมากกว่า
Tim Allclair

1
อันที่จริงผมสับอย่างรวดเร็วในรหัสมาตรฐานของคุณจะได้รับตัวเลขจริงบางอย่าง อาจมีบางอย่างที่ยังขาด / ผิดไปจากเกณฑ์มาตรฐานนี้ แต่สิ่งเดียวที่โค้ดสะท้อนที่ซับซ้อนกว่านี้เกิดขึ้นก็คือการตั้งค่านั้นเร็วขึ้น (ด้วย GOMAXPROCS = 1) เนื่องจากไม่จำเป็นต้องมี goroutines มากมาย ในกรณีอื่น ๆ ช่องทางการรวม goroutine แบบธรรมดาจะพัดพาโซลูชันสะท้อนแสงออกไป (ตามขนาด ~ 2 คำสั่ง)
Dave C

2
ข้อเสียที่สำคัญอย่างหนึ่ง (เมื่อเทียบกับreflect.Selectวิธีการ) คือ goroutines ที่ทำการรวมบัฟเฟอร์อย่างน้อยค่าเดียวในแต่ละช่องที่ถูกรวมเข้าด้วยกัน โดยปกติจะไม่เป็นปัญหา แต่ในแอปพลิเคชั่นบางตัวที่อาจเป็นตัวทำลายข้อตกลง :(
Dave C

1
ช่องทางการผสานบัฟเฟอร์ทำให้ปัญหาแย่ลง ปัญหาคือเฉพาะโซลูชันการสะท้อนเท่านั้นที่สามารถมีความหมายที่ไม่ได้บัฟเฟอร์อย่างสมบูรณ์ ฉันได้ดำเนินการต่อและโพสต์รหัสทดสอบที่ฉันทดลองเป็นคำตอบแยกต่างหาก (หวังว่า) จะชี้แจงสิ่งที่ฉันพยายามจะพูด
Dave C

22

หากต้องการขยายความคิดเห็นบางส่วนเกี่ยวกับคำตอบก่อนหน้านี้และเพื่อให้การเปรียบเทียบที่ชัดเจนขึ้นนี่คือตัวอย่างของทั้งสองแนวทางที่นำเสนอจนถึงตอนนี้โดยให้ข้อมูลเดียวกันช่องที่จะอ่านและฟังก์ชันในการเรียกค่าแต่ละค่าซึ่งจำเป็นต้องทราบด้วย ช่องค่ามาจาก

มีความแตกต่างหลักสามประการระหว่างแนวทาง:

  • ความซับซ้อน แม้ว่าบางส่วนอาจเป็นความชอบของผู้อ่าน แต่ฉันพบว่าแนวทางของช่องมีสำนวนตรงไปตรงมาและอ่านได้ง่ายกว่า

  • ประสิทธิภาพ. ในระบบ Xeon amd64 ของฉัน goroutines + channels out จะดำเนินการแก้ปัญหาการสะท้อนโดยประมาณสองลำดับของขนาด (โดยทั่วไปการสะท้อนใน Go มักจะช้ากว่าและควรใช้เมื่อจำเป็นจริงๆเท่านั้น) แน่นอนว่าหากมีความล่าช้าอย่างมีนัยสำคัญในฟังก์ชั่นการประมวลผลผลลัพธ์หรือในการเขียนค่าลงในช่องอินพุตความแตกต่างของประสิทธิภาพนี้อาจไม่สำคัญได้อย่างง่ายดาย

  • การปิดกั้น / การบัฟเฟอร์ความหมาย ความสำคัญของสิ่งนี้ขึ้นอยู่กับกรณีการใช้งาน ส่วนใหญ่มักจะไม่สำคัญหรือการบัฟเฟอร์เพิ่มเติมเล็กน้อยในโซลูชันการผสาน goroutine อาจเป็นประโยชน์สำหรับปริมาณงาน อย่างไรก็ตามหากเป็นที่พึงปรารถนาที่จะมีความหมายที่มีเพียงผู้เขียนคนเดียวเท่านั้นที่ถูกยกเลิกการปิดกั้นและค่าของมันได้รับการจัดการอย่างเต็มที่ก่อนที่นักเขียนคนอื่นจะถูกยกเลิกการปิดกั้นสิ่งนั้นสามารถทำได้ด้วยโซลูชันการสะท้อนเท่านั้น

หมายเหตุทั้งสองวิธีสามารถทำให้ง่ายขึ้นได้หากไม่จำเป็นต้องใช้ "id" ของช่องทางการส่งหรือถ้าช่องต้นทางจะไม่ถูกปิด

ช่องรวม Goroutine:

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

เลือกการสะท้อน:

// 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 playground ]


1
นอกจากนี้ยังเป็นที่น่าสังเกตว่าโซลูชัน goroutines + channels ไม่สามารถทำทุกอย่างselectหรือreflect.Selectทำได้ goroutines จะหมุนไปเรื่อย ๆ จนกว่าจะกินทุกอย่างจากช่องสัญญาณดังนั้นจึงไม่มีวิธีที่ชัดเจนที่คุณจะProcess1ออกก่อนเวลาได้ นอกจากนี้ยังมีศักยภาพในการเกิดปัญหาหากคุณมีผู้อ่านหลายตั้งแต่ goroutines selectบัฟเฟอร์หนึ่งรายการจากแต่ละช่องทางซึ่งจะไม่เกิดขึ้นกับ
James Henstridge

@JamesHenstridge บันทึกแรกของคุณเกี่ยวกับการหยุดไม่เป็นความจริง คุณจัดให้หยุด Process1 ในลักษณะเดียวกับที่คุณจัดให้หยุด Process2 เช่นเพิ่มช่อง "หยุด" ที่ปิดเมื่อ goroutines ควรหยุด Process1 ต้องการสองกรณีselectภายในforลูปแทนที่จะเป็นลูปที่ง่ายกว่าที่for rangeใช้ในปัจจุบัน Process2 จะต้องติดเคสอื่นเข้าไปcasesและจัดการกับค่าของi.
Dave C

นั่นยังไม่สามารถแก้ปัญหาที่คุณกำลังอ่านค่าจากช่องที่จะไม่ใช้ในกรณีหยุดก่อน
James Henstridge

0

ทำไมวิธีนี้ใช้ไม่ได้ถ้าสมมติว่ามีคนส่งเหตุการณ์

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
นี่คือสปินลูป ในขณะที่รอให้ช่องสัญญาณเข้ามีค่านี้จะใช้ CPU ทั้งหมดที่มี จุดรวมของselectช่องสัญญาณหลายช่อง (ไม่มีdefaultข้อ) คือการรออย่างมีประสิทธิภาพจนกว่าอย่างน้อยหนึ่งช่องจะพร้อมโดยไม่ต้องหมุน
Dave C

0

ตัวเลือกที่ง่ายกว่าอาจเป็นไปได้:

แทนที่จะมีอาร์เรย์ของแชนเนลทำไมไม่ส่งเพียงแชนเนลเดียวเป็นพารามิเตอร์ไปยังฟังก์ชันที่รันบนโกรูทีนที่แยกจากกันแล้วฟังแชนเนลในโกรูทีนของผู้บริโภค

วิธีนี้ช่วยให้คุณสามารถเลือกเพียงช่องเดียวในผู้ฟังของคุณทำให้เลือกได้ง่ายและหลีกเลี่ยงการสร้าง goroutines ใหม่เพื่อรวบรวมข้อความจากหลายช่องทาง?

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.