N 채널을 듣는 방법? (동적 선택 문)


116

두 개의 고 루틴을 실행하는 무한 루프를 시작하려면 아래 코드를 사용할 수 있습니다.

메시지를받은 후 새로운 고 루틴을 시작하고 영원히 계속됩니다.

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 고 루틴에 대해 동일한 동작을 원하지만이 경우 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
당신이 원하는 것은 채널 멀티플렉싱이라고 생각합니다. golang.org/doc/effective_go.html#chan_of_chan 기본적으로 청취하는 단일 채널이 하나 있고 기본 채널로 유입되는 여러 하위 채널이 있습니다. 관련 SO 질문 : stackoverflow.com/questions/10979608/…
Brenden

답변:


152

reflect 패키지 의 Select함수를 사용하여이를 수행 할 수 있습니다 .

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

Select는 케이스 목록에 설명 된 선택 작업을 실행합니다. Go select 문과 마찬가지로 케이스 중 하나 이상이 진행될 때까지 차단하고 균일 한 의사 랜덤 선택을 한 다음 해당 케이스를 실행합니다. 선택한 케이스의 인덱스를 반환하고 해당 케이스가 수신 작업 인 경우 수신 된 값과 해당 값이 채널의 전송에 해당하는지 여부를 나타내는 부울을 반환합니다 (채널이 닫혀 있기 때문에 수신 된 0 값이 아님).

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가 말했듯이 공유 "집계"채널을 전달하는 것은 제 경우 훨씬 쉬웠습니다.
보라 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)
}

메시지가 시작된 채널을 알아야하는 경우 집계 채널로 전달하기 전에 추가 정보가있는 구조체로 래핑 할 수 있습니다.

내 (제한된) 테스트 에서이 방법은 reflect 패키지를 사용하여 크게 수행됩니다.

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

여기에 벤치 마크 코드


2
벤치 마크 코드가 올바르지 않습니다 . 벤치 마크 내 에서 반복b.N 해야 합니다 . 그렇지 않으면 결과 ( b.N출력에서, 1 및 2000000000로 나눈 값 )는 완전히 의미가 없습니다.
Dave C

2
@DaveC 감사합니다! 결론은 변하지 않지만 결과는 훨씬 더 정상적입니다.
Tim Allclair 2015 년

1
실제로 실제 수치 를 얻기 위해 벤치 마크 코드를 빠르게 해킹했습니다 . 이 벤치 마크에서 여전히 누락 / 잘못된 부분이있을 수 있지만 더 복잡한 리플렉션 코드의 유일한 점은 고 루틴이 필요하지 않기 때문에 설정이 더 빠르다는 것입니다 (GOMAXPROCS = 1 사용). 다른 모든 경우에 간단한 고 루틴 병합 채널이 반사 솔루션을 날려 버립니다 (약 2 배 정도).
Dave C

2
reflect.Select접근 방식 과 비교하여 한 가지 중요한 단점 은 고 ​​루틴이 병합되는 각 채널에서 최소 단일 값으로 병합 버퍼를 수행한다는 것입니다. 일반적으로 문제가되지는 않지만 거래 중단자가 될 수있는 일부 특정 응용 프로그램에서는 :(.
Dave C

1
버퍼링 된 병합 채널은 문제를 더 악화시킵니다. 문제는 reflect 솔루션 만이 완전히 버퍼링되지 않은 의미를 가질 수 있다는 것입니다. 나는 계속해서 실험하고 있던 테스트 코드를 (희망적으로) 내가 말하려는 것을 명확히하기위한 별도의 답변으로 게시했습니다.
Dave C

22

이전 답변에 대한 일부 의견을 확장하고 여기에 더 명확한 비교를 제공하기 위해 동일한 입력, 읽을 채널 조각 및 각 값을 호출하는 함수가 주어 졌을 때 지금까지 제시된 두 접근 방식의 예가 있습니다. 채널 가치의 출처.

접근 방식에는 세 가지 주요 차이점이 있습니다.

  • 복잡성. 부분적으로 독자 선호도 일 수 있지만 채널 접근 방식이 더 관용적이고 간단하며 읽기 쉽다고 생각합니다.

  • 공연. 제 Xeon amd64 시스템에서 goroutines + channels out은 약 2 배 정도의 반사 솔루션을 수행합니다 (일반적으로 Go의 반사는 종종 더 느리며 절대적으로 필요한 경우에만 사용해야 함). 물론 결과를 처리하는 함수 나 입력 채널에 값을 쓰는 데 상당한 지연이있는 경우 이러한 성능 차이는 쉽게 미미해질 수 있습니다.

  • 차단 / 버퍼링 의미론. 이것의 중요성은 사용 사례에 따라 다릅니다. 대부분 중요하지 않거나 고 루틴 병합 솔루션의 약간의 추가 버퍼링이 처리량에 도움이 될 수 있습니다. 그러나 단일 작성자 만 차단 해제되고 다른 작성자가 차단 해제 되기 전에 값이 완전히 처리된다는 의미를 갖는 것이 바람직하다면 reflect 솔루션으로 만 달성 할 수 있습니다.

전송 채널의 "id"가 필요하지 않거나 소스 채널이 닫히지 않는 경우 두 가지 방법을 모두 단순화 할 수 있습니다.

고 루틴 병합 채널 :

// 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 플레이 그라운드의 전체 코드 .]


1
또한 goroutines + 채널 솔루션은 모든 것을 할 수 있다는 것을 주목할 필요가 select또는 reflect.Select않습니다. 고 루틴은 채널에서 모든 것을 소비 할 때까지 계속 회전하므로 Process1일찍 종료 할 수있는 명확한 방법이 없습니다 . 고 루틴이 각 채널에서 하나의 항목을 버퍼링하기 때문에 여러 리더가있는 경우 문제가 발생할 가능성이 있습니다 select.
James Henstridge 2015

@JamesHenstridge, 중지에 대한 첫 번째 메모는 사실이 아닙니다. Process2를 중지 할 때와 똑같은 방법으로 Process1을 중지 할 수 있습니다. 예를 들어 고 루틴이 중지되어야 할 때 닫히는 "중지"채널이 추가되었습니다. Process1은 현재 사용되는 더 간단한 루프 대신 루프 select내에 두 개의 케이스가 필요합니다 . Process2는 다른 케이스를 삽입 하고 해당 값을 특수 처리해야합니다 . forfor rangecasesi
Dave C

그래도 조기 중지 사례에서 사용되지 않는 채널에서 값을 읽는 문제는 해결되지 않습니다.
James Henstridge 2016 년

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

더 간단한 옵션 :

채널 배열을 갖는 대신 별도의 고 루틴에서 실행되는 함수에 매개 변수로 하나의 채널 만 전달한 다음 소비자 고 루틴에서 채널을 수신하는 것이 어떻습니까?

이렇게하면 리스너에서 하나의 채널 만 선택할 수 있으므로 간단한 선택이 가능하며 여러 채널의 메시지를 집계하기 위해 새 고 루틴을 만들지 않아도됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.