як слухати 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 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
Я думаю, що ти хочеш - мультиплексування каналів. 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
Чи є практичне обмеження кількості справ у такому відборі? Той, що якщо ви виходите за його межі, то на продуктивність сильно впливає?
Максим Володимирський

4
Можливо, це моя некомпетентність, але мені ця модель дуже важко працювати, коли ви надсилаєте та отримуєте складні структури через канал. Як проголосив Тім Олклар, пройти загальний "сукупний" канал у моєму випадку було набагато простіше.
Бора М. Альпер

90

Ви можете досягти цього, загорнувши кожен канал у програму, яка "пересилає" повідомлення до спільного "сукупного" каналу. Наприклад:

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

Код орієнтиру тут


2
Ваш контрольний код невірний, вам потрібно перетворити циклb.N на тест. Інакше результати (які розділені на b.N1 і 2000000000 у вашому результаті) будуть абсолютно безглуздими.
Дейв C

2
@DaveC Дякую! Висновок не змінюється, але результати набагато розумніші.
Тім Оллклар

1
Дійсно, я швидко зламав ваш базовий код, щоб отримати деякі фактичні цифри . З цього еталону може бути щось все ще відсутнє / неправильне, але єдине, що має складніший відбиваючий код - це налаштування швидше (з GOMAXPROCS = 1), оскільки йому не потрібна купа гарантій. У будь-якому іншому випадку простий канал, що зливається, виконує відображення рішення (на ~ 2 порядки).
Дейв C

2
Одним з важливих недоліків (порівняно з reflect.Selectпідходом) є те, що ті програми, які роблять буфер, що зливається, як мінімум одне значення на кожному каналі, що об'єднується. Зазвичай це не буде проблемою, але в деяких конкретних програмах, які можуть бути порушниками угоди :(.
Дейв C,

1
буферизований канал злиття погіршує проблему. Проблема полягає в тому, що лише рішення, що відображає, може мати повністю не буферизовану семантику. Я пішов вперед і розмістив тестовий код, з яким експериментував, як окрему відповідь на (сподіваюся) уточнення того, що я намагався сказати.
Дейв C

22

Щоб розширити деякі коментарі до попередніх відповідей і надати більш чітке порівняння, ось приклад обох підходів, представлених до цих пір, дається однаковий вхід, фрагмент каналів для читання та функція для виклику кожного значення, яке також повинно знати, який канал, значення якого прийшло.

Існує три основні відмінності між підходами:

  • Складність. Хоча частково це може бути перевагою читача, я вважаю, що підхід до каналу є більш ідіоматичним, прямим та зрозумілим.

  • Продуктивність. У моїй системі Xeon amd64 goroutines + канали виконує рішення відображення приблизно на два порядки (загалом, відображення в Go часто буває повільнішим і його слід використовувати лише за необхідності). Звичайно, якщо є якась значна затримка або в функції, що обробляє результати, або в записі значень на вхідні канали, ця різниця в продуктивності може легко стати незначною.

  • Семантика блокування / буферизації. Важливість цього залежить від випадку використання. Найчастіше це або не має значення, або незначна додаткова буферизація в розчині об’єднання goroutin може бути корисною для пропускної здатності. Однак якщо бажано мати семантику того, що розблокується лише один автор, і цінність його повністю обробляється до того, як розблокується будь-який інший письменник, то цього можна досягти лише за допомогою роздумування.

Зауважте, обидва підходи можуть бути спрощені, якщо або "ідентифікатор" каналу відправки не потрібен, або якщо вихідні канали ніколи не будуть закриті.

Канал об’єднання програми:

// 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
Варто також зазначити, що рішення goutut + каналів не може зробити все selectабо reflect.Selectробить. Гораути продовжуватимуть обертатися, поки вони не споживають все з каналів, тому немає чіткого способу, як ви могли б Process1виїхати рано. Існує також можливість виникнення проблем, якщо у вас є декілька читачів, оскільки goututines буферує один елемент з кожного з каналів, що не відбудеться select.
Джеймс Генстридж

@JamesHenstridge, ваша перша примітка про зупинку не відповідає дійсності. Ви б домовилися зупинити Process1 точно так само, як і ви зупинили Process2; наприклад, додавши канал "стоп", який закривається, коли гарантії повинні зупинитися. Process1 знадобиться два випадку selectв forциклі замість більш простого for rangeциклу, який використовується в даний час. Process2 потрібно було б вставити інший випадок у casesта спеціальну ручку, що має значення i.
Дейв C

Це все ще не вирішує проблему з тим, що ви читаєте значення з каналів, які не будуть використані у випадку раннього зупинки.
Джеймс Генстридж

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
Це спін-петля. Під час очікування значення вхідного каналу витрачається весь доступний процесор. Вся суть selectна декількох каналах (без defaultпункту) полягає в тому, що він ефективно чекає, поки щонайменше один не буде готовий, не обертаючись.
Дейв C

0

Можливо простіший варіант:

Замість того, щоб мати масив каналів, чому б не передати лише один канал як параметр функціям, що виконуються в окремих програмах, а потім прослухати канал у споживчій програмі?

Це дозволяє вам обрати лише один канал у вашому слухачі, зробити простий вибір і уникнути створення нових програм для об'єднання повідомлень з декількох каналів?

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.