почему результат вложенной горутины не выводится и как это исправить?

Бьюсь за понимание горутин и каналов. Тема для меня оказалась сложной и один момент не дает мне покоя(так как я его не понимаю). Я приведу полный код, так как не уверен, что проблема в функции somework(хотя почти наверняка в ней).

Данная задача является полностью выдуманной чтобы разобраться:

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

Все свои мысли я написал в комментах к коду, в т.ч пометил место, которое у меня вызвает вопрос. Поэтому welcome сразу в код. Спасибо за ваше внимание и время.

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "time"
)

func main() {
    start := time.Now().UnixNano()
    in, out, wait := make(chan int), make(chan int), make(chan bool)
    go somework(in, out, wait, 12)
    // записываем рандомные данные в канал который потом
    // передадим в качестве аргумена в somework
    in <- int(time.Now().Unix())
    // выводим результат работы somework
    fmt.Printf("somework вернула в канале значение: %v\n", <-out)
    end := time.Now().UnixNano()
    fmt.Printf("функция main завершилась через >>%v<< наносекунд после своего начала, выполнив все второстепенные горутины\n", end-start)
    // ждем, пока somework просигнализирует об окончании своей работы.
    <-wait
}

// некая функция, которая внутри себя должна отправить на выполнение длительную операцию.
// все горутины, включая main должны продолжать свое выполнение и когда завершится длительная
// операция вывести результат этой длительной операции независмо от того что в этот момент
// будет делать функция main
func somework(ch, out chan int, wait chan bool, i int) chan int {
    start := time.Now().UnixNano()
    go func() {
        rsultOfrollingAction := make(chan int)
        // далее запускаю длительную операцию чтения файла в отдельной горутине
        go func(rsultOfrollingAction chan int) {
            d := make(chan bool) //для сигнализации об окончании длительного действия
            // начало длительной операции в отдельной горутине
            // результат данной работы, при имеющемся коде, не выводится.
            // ДАННЫЙ ВОПРОС ОТНОСТИИТСЯ КАК РАЗ К ЭТОЙ ПРОБЛЕМЕ
            go func(d chan bool) {
                time.Sleep(time.Second * 3)
                text := readf("text.txt")
                rsultOfrollingAction <- len(text)
                // сигнализируем горутине которая должна вывести количество
                // прочитанных строк, что длительная операция завершене
                d <- true
            }(d)
            // когда d получает сигнал об окончании длительной операции
            <-d
            // выводим результат длительной операции.
            fmt.Printf("вложенная в somework горутина завершила выполенение прочитав из файла %v символов\n", <-rsultOfrollingAction)
        }(rsultOfrollingAction)
        // читаем канал из входных аргументов somework
        a := <-ch
        // записываем в вызодной кнал
        out <- a
        // сигнализируем об окончании somesomework()
        wait <- true
    }()
    fmt.Printf("прошло >>%v<< наносекунд с момента начала функции work и функция somework завершилась передав управление main\n", time.Now().UnixNano()-start)
    // возвращаем канал с результататом работы somework
    return out
}

// поростейшее чтение файла.
func readf(name string) []byte {
    f, err := ioutil.ReadFile(name)
    if err != nil {
        fmt.Println(err)
        os.Exit(123)
    }
    return f
}

Ответы (1 шт):

Автор решения: Pak Uula

Первая проблема

В канал wait вы пишете не в той функции. Смотрите, что у вас происходит:

main порождает somework и продолжает работу. Затем останавливается, ожидая канал out.

somework порождает вложенную горутину и завершается.

Стартует первая анонимная горутина, которую рантайм Го обозначает как main.somework.func1. Она

  1. порождает анонимную горутину main.somework.func1.1
  2. читает канал in, записывает в канал out и
  3. ТАДАМ записывает в канал wait

Где-то между 2 и 3 разблокируется main, читая из канала out. Затем main снова блокируется, ожидая из канала wait. И в тот же момент main снова пробуждается, так как горутина main.somework.func1 записала в wait.

main печатает сообщение и ЗАВЕРШАЕТСЯ. Есть ненулевая вероятность, что вторая анонимная горутина main.somework.func1.1 даже не успела стартануть.

Вам нужно перенести wait <- true в ту горутину, которая ожидает завершения чтения из файла:

    go func() {
        rsultOfrollingAction := make(chan int)
        // далее запускаю длительную операцию чтения файла в отдельной горутине
        go func(rsultOfrollingAction chan int) {
            d := make(chan bool) //для сигнализации об окончании длительного действия
            // начало длительной операции в отдельной горутине
            // результат данной работы, при имеющемся коде, не выводится.
            // ДАННЫЙ ВОПРОС ОТНОСТИИТСЯ КАК РАЗ К ЭТОЙ ПРОБЛЕМЕ
            go func(d chan bool) {
                time.Sleep(time.Second * 3)
                text := readf("sample.bin")
                rsultOfrollingAction <- len(text)
                // сигнализируем горутине которая должна вывести количество
                // прочитанных строк, что длительная операция завершене
                d <- true
            }(d)
            // когда d получает сигнал об окончании длительной операции
            <-d
            // выводим результат длительной операции.
            fmt.Printf("вложенная в somework горутина завершила выполенение прочитав из файла %v символов\n", <-rsultOfrollingAction)
            wait <- true
        }(rsultOfrollingAction)
        // читаем канал из входных аргументов somework
        a := <-in
        // записываем в вызодной кнал
        out <- a
        // сигнализируем об окончании somesomework()
        // wait <- true
    }()

Видите? Я перенёс запись в wait из первой вложенной горутины во вторую, так как вторая горутина точно дождётся чтения из файла благодаря синхронизации через d и rsultOfrollingAction. Именно она должна сигнализировать, что длинная операция завершена.

Вторая проблема

При такой синхронизации образуется дедлок. Причина в том, что инструкция make(chan T) создаёт канал нулевой ёмкости. То есть читатель блокируется, если нет писателя, писатель блокируется, если нет читателя. Что и просиходит у вас:

  • вторая анонимная функция ждёт канал d (<-d). Так как в момент ожидания d третья горутина ещё не стартовала, то вторая горутина блокируется.
  • третья анонимная горутина ждёт, потом читает файл, потом записывает результат в rsultOfrollingAction (rsultOfrollingAction <- len(text)). И ... тоже блокируется! Так как у канала rsultOfrollingAction в этот момент ещё нет читателя. Читатель - вторая горутина - заблокирована на ожидании d.

Что с этим можно сделать.

План А.

Выбросить d. Появление данных в rsultOfrollingAction само собой обозначает, что операция чтения завершена.

Пруф: https://go.dev/play/p/mXxkYgXkyxw

main дождается выполнения вложенных горутин.

План Б

Если канал d вам дорог сам по себе, то нужно сделать канал rsultOfrollingAction асинхронным: rsultOfrollingAction := make(chan int, 1)

Если канал пуст, писатель не будет ждать появления читателя. Записанное значение будет записано в буфер канала, и третья горутина благополучно перейдёт к следующей операции d <- true

Пруф: https://go.dev/play/p/4BeRqa2ilJT

→ Ссылка