утечка памяти в решении для упражнения: эквивалентные двоичные деревья? - PullRequest
3 голосов
/ 26 марта 2019

(https://github.com/golang/tour/blob/master/solutions/binarytrees_quit.go) для упражнения: эквивалентные двоичные деревья Предположим, у нас есть два простых эквивалентных двоичных дерева "1 3 5" и "2 3 5". Когда две goroutines "Прогулка" идет по листу "1" и "2 "одновременно,

if v1 != v2 {
    return false
}

это условие в функции Same будет верным и

close(quit)

будет выполняться.

func walkImpl(t *tree.Tree, ch, quit chan int) {
    if t == nil {
        return
    }
    walkImpl(t.Left, ch, quit)
    select {
    case ch <- t.Value:
        // Value successfully sent.
    case <-quit:
        return
    }
    walkImpl(t.Right, ch, quit)
}

Канал"quit" получит сообщение, и будет выполнен второй случай оператора select. Затем он вернется к функции верхнего уровня "walkImpl" и продолжит выполнение последней строки walkImpl(t.Right, ch, quit). Так что есть ли утечка goroutine при данных обстоятельствах,причина, по которой канал "quit" уже считан, который не может быть прочитан снова на верхнем уровне? Функция "Walk" также не может вернуться к обработчику "close".

1 Ответ

2 голосов
/ 26 марта 2019

Когда для нескольких процедур назначается сигнал отмены, чаще всего это делается путем закрытия канала, а не отправки значения в канал. Прием из закрытого канала может начаться немедленно, независимо от того, сколько это выполнено. Значение, отправленное по каналу, может быть получено не более одного раза, поэтому он не подходит для того, чтобы сигнализировать множеству процедур со значением. Спецификация: Оператор получения:

Операция приема на закрытом канале всегда может выполняться немедленно, давая нулевое значение типа элемента после получения любых ранее отправленных значений.

Теперь, если вы закроете канал quit, это не гарантирует, что ваша функция вернется немедленно.

Сначала вы возвращаетесь к левому ребенку, не проверяя quit, который будет делать то же самое (пока не будет достигнут nil левый ребенок).

Во-вторых, если значение может быть отправлено на ch, тогда оба случая готовы и, таким образом, select выбирает один из них случайным образом, что может быть или не быть случаем quit. Подробнее см. Как работает выбор, когда задействованы несколько каналов?

Если вы хотите избежать этого, вы должны добавить неблокирующую проверку quit в качестве первой вещи в вашей функции:

func walkImpl(t *tree.Tree, ch, quit chan int) {
    select {
    case <-quit:
        return
    default: // This empty default makes it a non-blocking check
    }

    if t == nil {
        return
    }
    walkImpl(t.Left, ch, quit)
    select {
    case ch <- t.Value:
        // Value successfully sent.
    case <-quit:
        return
    }
    walkImpl(t.Right, ch, quit)
}

Теперь можно было бы спросить, нужен ли нам еще случай quit во втором select, поскольку мы уже проверили его первым делом в walkImpl(). Ответ заключается в том, что вы также должны сохранить это, потому что, если отправка на ch заблокирует (например, потребитель будет отключен, когда quit будет закрыт), эта операция отправки может заблокироваться навсегда. Таким образом (когда quit закрыто) гарантируется, что функция вернется.

...