làm thế nào để nghe N kênh? (câu lệnh chọn động)


116

để bắt đầu một vòng lặp vô tận thực hiện hai goroutines, tôi có thể sử dụng đoạn mã dưới đây:

sau khi nhận được tin nhắn, nó sẽ bắt đầu một quy trình mới và tiếp tục mãi mãi.

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

Bây giờ tôi muốn có hành vi tương tự cho N goroutines, nhưng câu lệnh select sẽ trông như thế nào trong trường hợp đó?

Đây là bit mã tôi đã bắt đầu, nhưng tôi bối rối làm thế nào để viết mã câu lệnh 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
Tôi nghĩ những gì bạn muốn là Ghép kênh. golang.org/doc/effective_go.html#chan_of_chan Về cơ bản, bạn có một kênh duy nhất để nghe và sau đó có nhiều kênh con ghép vào kênh chính. Câu hỏi SO liên quan: stackoverflow.com/questions/10979608/…
Brenden,

Câu trả lời:


152

Bạn có thể thực hiện việc này bằng cách sử dụng Selectchức năng từ gói phản ánh :

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

Chọn thực hiện một thao tác chọn được mô tả bởi danh sách các trường hợp. Giống như câu lệnh Go select, nó chặn cho đến khi ít nhất một trong các trường hợp có thể tiến hành, đưa ra lựa chọn giả ngẫu nhiên thống nhất và sau đó thực hiện trường hợp đó. Nó trả về chỉ mục của trường hợp đã chọn và, nếu trường hợp đó là hoạt động nhận, thì giá trị nhận được và boolean cho biết liệu giá trị có tương ứng với một lần gửi trên kênh hay không (trái ngược với giá trị 0 nhận được vì kênh bị đóng).

Bạn chuyển vào một mảng SelectCasecấu trúc xác định kênh để chọn, hướng hoạt động và giá trị để gửi trong trường hợp hoạt động gửi.

Vì vậy, bạn có thể làm một cái gì đó như sau:

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

Bạn có thể thử nghiệm với một ví dụ cụ thể hơn tại đây: http://play.golang.org/p/8zwvSk4kjx


4
Có giới hạn thực tế nào đối với số trường hợp được chọn như vậy không? Cái mà nếu bạn vượt quá nó, thì hiệu suất sẽ bị ảnh hưởng nghiêm trọng?
Maxim Vladimirsky

4
Có thể đó là sự kém cỏi của tôi, nhưng tôi thấy mô hình này thực sự khó làm việc khi bạn gửi và nhận các cấu trúc phức tạp qua kênh. Theo Tim Allclair, việc vượt qua một kênh "tổng hợp" được chia sẻ dễ dàng hơn nhiều trong trường hợp của tôi.
Bora M. Alper,

90

Bạn có thể thực hiện điều này bằng cách gói mỗi kênh trong một quy trình "chuyển tiếp" thông báo đến một kênh "tổng hợp" được chia sẻ. Ví dụ:

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

Nếu bạn cần biết thông báo bắt nguồn từ kênh nào, bạn có thể bao bọc nó trong một cấu trúc với bất kỳ thông tin bổ sung nào trước khi chuyển tiếp nó đến kênh tổng hợp.

Trong thử nghiệm (có giới hạn) của tôi, phương pháp này hoạt động rất hiệu quả bằng cách sử dụng gói phản ánh:

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

Mã điểm chuẩn tại đây


2
Mã điểm chuẩn của bạn không chính xác, bạn cần lặp lạib.N trong điểm chuẩn. Nếu không, kết quả (chia cho b.N1 và 2000000000 trong đầu ra của bạn) sẽ hoàn toàn vô nghĩa.
Dave C

2
@DaveC Cảm ơn bạn! Kết luận không thay đổi, nhưng kết quả là lành mạnh hơn nhiều.
Tim Allclair

1
Thật vậy, tôi đã hack nhanh mã điểm chuẩn của bạn để lấy một số con số thực tế . Rất có thể có điều gì đó vẫn còn thiếu / sai từ điểm chuẩn này nhưng điều duy nhất mà mã phản ánh phức tạp hơn đã xảy ra với nó là thiết lập nhanh hơn (với GOMAXPROCS = 1) vì nó không cần nhiều goroutines. Trong mọi trường hợp khác, một kênh hợp nhất theo quy trình đơn giản sẽ thổi bay giải pháp phản xạ (khoảng ~ 2 bậc độ lớn).
Dave C

2
Một nhược điểm quan trọng (so với reflect.Selectcách tiếp cận) là các goroutines thực hiện bộ đệm hợp nhất ở mức tối thiểu một giá trị duy nhất trên mỗi kênh được hợp nhất. Thông thường đó sẽ không là một vấn đề nhưng trong một số ứng dụng cụ thể mà có thể là một đối phó breaker :(.
Dave C

1
kênh hợp nhất có bộ đệm làm cho vấn đề trở nên tồi tệ hơn. Vấn đề là chỉ có giải pháp phản ánh mới có thể có ngữ nghĩa hoàn toàn không đệm. Tôi đã tiếp tục và đăng mã thử nghiệm mà tôi đang thử nghiệm như một câu trả lời riêng để (hy vọng) làm rõ những gì tôi đang cố gắng nói.
Dave C

22

Để mở rộng một số nhận xét về các câu trả lời trước đó và để cung cấp một so sánh rõ ràng hơn, đây là một ví dụ về cả hai cách tiếp cận được trình bày cho đến nay với cùng một đầu vào, một phần kênh để đọc và một hàm để gọi cho từng giá trị cũng cần biết kênh giá trị đến từ.

Có ba điểm khác biệt chính giữa các cách tiếp cận:

  • Tính phức tạp. Mặc dù nó có thể một phần là sở thích của người đọc, tôi thấy cách tiếp cận kênh mang tính thành ngữ, dễ hiểu và dễ đọc hơn.

  • Hiệu suất. Trên hệ thống Xeon amd64 của tôi, goroutines + channel out thực hiện giải pháp phản xạ khoảng hai bậc độ lớn (nói chung phản xạ trong cờ vây thường chậm hơn và chỉ nên được sử dụng khi thực sự cần thiết). Tất nhiên, nếu có bất kỳ độ trễ đáng kể nào trong việc chức năng xử lý kết quả hoặc trong việc ghi giá trị vào các kênh đầu vào, sự khác biệt về hiệu suất có thể dễ dàng trở nên không đáng kể.

  • Chặn / đệm ngữ nghĩa. Tầm quan trọng của điều này phụ thuộc vào trường hợp sử dụng. Thông thường, nó sẽ không thành vấn đề hoặc việc đệm thêm một chút trong giải pháp hợp nhất theo quy trình có thể hữu ích cho thông lượng. Tuy nhiên, nếu bạn muốn có ngữ nghĩa mà chỉ một người viết duy nhất được bỏ chặn và giá trị của nó được xử lý hoàn toàn trước khi bất kỳ người viết nào khác được bỏ chặn, thì điều đó chỉ có thể đạt được với giải pháp phản ánh.

Lưu ý, có thể đơn giản hóa cả hai cách tiếp cận nếu "id" của kênh gửi không bắt buộc hoặc nếu các kênh nguồn sẽ không bao giờ bị đóng.

Kênh hợp nhất theo quy trình:

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

Lựa chọn phản chiếu:

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

[Toàn mã trên sân chơi cờ vây .]


1
Nó cũng đáng chú ý là giải pháp goroutines kênh + không thể làm tất cả mọi thứ selecthay reflect.Selectkhông. Các goroutines sẽ tiếp tục quay cho đến khi chúng tiêu thụ hết mọi thứ từ các kênh, vì vậy không có cách nào rõ ràng để bạn có thể Process1thoát sớm. Cũng có khả năng xảy ra sự cố nếu bạn có nhiều đầu đọc, vì các goroutines đệm một mục từ mỗi kênh, điều này sẽ không xảy ra với select.
James Henstridge

@JamesHenstridge, lưu ý đầu tiên của bạn về việc dừng lại là không đúng. Bạn sẽ sắp xếp để dừng Process1 giống hệt như cách bạn sắp xếp để dừng Process2; ví dụ bằng cách thêm một kênh "dừng" được đóng lại khi các tuyến sinh dục nên dừng lại. Process1 sẽ cần một hai trường hợp selecttrong một forvòng lặp thay vì for rangevòng lặp đơn giản hơn hiện được sử dụng. Process2 sẽ cần gắn một trường hợp khác vào casesvà xử lý đặc biệt giá trị đó của i.
Dave C

Điều đó vẫn không giải quyết được vấn đề bạn đang đọc các giá trị từ các kênh sẽ không được sử dụng trong trường hợp dừng sớm.
James Henstridge

0

Tại sao cách tiếp cận này sẽ không hoạt động giả sử rằng ai đó đang gửi sự kiện?

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
Đây là một vòng quay. Trong khi chờ một kênh đầu vào có giá trị, điều này sẽ tiêu thụ tất cả CPU có sẵn. Toàn bộ điểm của selectnhiều kênh (không có defaultđiều khoản) là nó có hiệu quả đợi cho đến khi ít nhất một kênh sẵn sàng mà không quay.
Dave C

0

Tùy chọn có thể đơn giản hơn:

Thay vì có một mảng kênh, tại sao không chuyển chỉ một kênh làm tham số cho các chức năng đang được chạy trên các goroutines riêng biệt, và sau đó lắng nghe kênh đó trong goroutine của người tiêu dùng?

Điều này cho phép bạn chỉ chọn trên một kênh trong trình nghe của mình, thực hiện một lựa chọn đơn giản và tránh tạo các goroutines mới để tổng hợp tin nhắn từ nhiều kênh?

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.