Как стать автором
Обновить

Комментарии 31

Вопрос знатокам: как прервать выполнение go routine с io.Copy внутри? Сразу скажу, что блокировка происходит на этой строке https://golang.org/src/io/io.go#L380. Интерфейс Close тоже не помогает.
Пока мне никто не смог ответить на этот вопрос.

Я думаю, тут нет универсального совета — если Read заблокирован, то это блок на уровне сискола. Close() может остановить чтение в случае, скажем, сетевого соединения, но не при чтении из файла, и это ещё может отличаться от ОС к ОС.
Если ваш кейс — чтение файлов и ОС Linux, то может помочь вот этот пакет — https://github.com/npat-efault/poller
Откуда чтение происходит? Файл на диске? Сокет? Для файлов Go не делает ничего особенного, просто вызывает syscall. Можете для каждой ОС поискать, что делать в случае блокировки. Если сокет, то в Go это пройдет через асинхронную прослойку, специфичную для конкретной ОС. В этом случае блокировка снимется, если закрыть сокет из какой-либо другой горутины или с другого конца соединения. Опять же, все работает так же, как если бы писали сами на С.

В моем случае это stdin и stdout.

Пошлите соответствующему треду сигнал, на который установлен обработчик.

И как этот тред завершит io.Copy, если src.Read(buf) заблокирован?

Сигнал (man 2 kill) прерывает заблокированный системный вызов, тот возвращается с errno == EINTR.

go routine это же легковесный тред. тред процесса нельзя завершить не завершив сам процесс. если же ты говоришь о сигнале, который передаётся через канал, то Read(buf) блокирует всю go routine функцию, и нет возможности обработать сигнал, полученный с канала.

Речь идет про POSIX сигналы. Никакого завершения тредов не происходит. Нужно установить обработчик сигнала, так же, как делает sigaction(3) (предположительно, в go есть для этого интерфейс), а потом послать заблокированному треду сигнал, например SIGINTR. В случае, если чтение идет с терминала это эквивалентно нажатию ^C пользователем. Системный вызов read немедленно вернется с EINTR.

Может я тебя не так понял, но вот пример. Висит, пока его не прибьёшь KILL'ом.
https://play.golang.org/p/s2_qrgnbPQ
Потом как ты себе представляешь production приложение, которое само себе сигналы шлет? Тесты уж куда ни шло.

В чем проблема слать себе сигналы? Это стандартный POSIX интерфейс, с четкой семантикой.

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

Почтальон звонит (дважды). :-)

Если интерес всё еще не пропал, то ниже выложил пример, который необходимо заставить правильно работать. https://habrahabr.ru/post/306914/#comment_9733560

И жутко асинхронный. Ну пошлёт горутина сигнал, а ну как он прилетит когда read() уже отработал и Go уже начал обработку совершенно другой горутины? Ведь это вполне возможно, между принятием решения и, собственно, посылкой сигнала проходит время. Прилетает он тоже не мгновенно.


Полагаю, обработчик в этом случае «просто ничего не делай»: read и так вернётся с EINTR, а влияния на код никакого не будет… если только «совершенно другая горутина» не начала свой read, который вы немедленно прибьёте задолго до timeout’а (если он вообще будет нужен).


И ещё, откуда вы собрались добывать актуальный PID нужной нити? Go может взять и переместить горутину с одной системной нити на другую, никаких гарантий тут нет.


В общем, это попытка подковать блоху кузнечным молотом. Если руки алмазные, то пройдёт, но, скорее, добавите себе пачку ошибок.

Справедливости ради, горутина скорее всего будет висеть на одном потоке и никуда не денется. Она заблокирована на syscall, а для этого им выделяется отдельный поток, который нигде больше не участвует кроме как для ожидания возвращения управления. Найти его PID можно с помощью ОС зависимых библиотек, я это успешно делал. Для пущей уверенности можно закрепить поток за горутиной с помощью «LockOSThread», хоть не известно, сохранится ли этот поток за горутиной при syscall или выделится новый.

В общем можно, но сломаться это может в любой момент, когда чего-нить в планировщике поменяют.
Т.е. это обычный файловый дескриптор, значит висит syscall. Виснет это точно так же, как зависла бы в C или где-либо еще попытка чтения из stdin без каких-либо входящих данных. Тут или писать код, который умеет с этим работать, т.е. UNIX/Go way. Либо пытаться прикрутить асинхронность с таймаутами как советовали выше с помощью отдельной библиотеки. Мой совет, лучше подумать над архитектурой приложения, чтобы блокировку на stdin не нужно было посреди работы приложения завершать. Можете завести одну горутину, которая будет висеть и читать из stdin. Дальше уже можете на каналах с таймаутами реализовать все то, что нужно, чем городить epoll'ы и select'ы в сторонних библиотеках.
Нет желания разбираться, что у вас там конкретно как работает, но все более менее варианты вроде перечислили, даже с сигналами. Либо выделить одну горутину на stdin и через каналы пробрасывать из нее ввод дальше, либо пробовать асинхронность с таймаутами посредством сторонней библиотеки. Собственно, проблема не специфична для Go. Вообще, сам io.Copy вызов тут скорее всего не сильно уместен. Он отлично подходит для сокетов. Как только один из концов закроется или вылетит какая-либо ошибка, то все прервется и можно завершить работу. Но здесь единственное условие выхода это действие пользователя.

Таймауты не вариант, они решают только последствия частных случаев. Даже если выделить goroutine на stdin — stdin останется открытым и следующая go routine не сможет с ним нормально работать, пока пользователь не нажмет enter.


я пробовал закрыть файловый дескриптор на stdin, тогда goroutine завершается. но и терминал перестаёт работать.

> Даже если выделить goroutine на stdin — stdin останется открытым и следующая go routine не сможет с ним нормально работать
в смысле последующая?

func input(dc chan []byte) {
    defer close(dc)
    for {
        data := make([]byte, 128)
        n, err := os.Stdin.Read(data)
        if n > 0 {
            dc <- data[0:n]
        }
        if err != nil {
            break
        }
    }
}

вот такая горутина будет читать из stdin и писать в канал а дальше уже где нужно делаете worker, process или что у вас по смыслу где будете читать из этого канала:

func worker(dc chan []byte, done chan bool) {
    select {
        case data, ok := <- dc:
            if !ok {
                // chanel closed exit
                return
            }

            // do some work
        case <- done:
            // exit
            return 
    }
}

соответственно когда нужно завершить эту горутину пишите в канал done
внутри worker забыл for вокруг select написать, но думаю общий смысл передан
Ага, тот самый паттерн, который циркулирует в сети на эту тему. В общем-то, самый идиоматичный вариант.

Эту, это какую? worker? А какой смысл мне её завершать? Мне в данном случае требуется завершить input и прервать os.Stdin.Read(). Чтобы в следующем цикле я бы смог использовать os.Stdin для других целей.

@neolink меня поправит, если что, но думаю, имелось в виду другое — вы создаёте себе одну горутину, которая у вас будет создаваться сразу на старте, жить до самого завершения вашей программы, и задачей у неё будет читать os.Stdin и писать всё прочитанное в канал. (Или pipe, например, можно попробовать.) Тогда этот канал становится вашим stdin, вы можете завершать одних его слушателей и назначать других по необходимости. А горутина, которая читает os.Stdin, завершится сама на выходе приложения.

Вот пример кода, который необходимо заставить правильно работать:


https://gist.github.com/kayrus/2753b4710e78dd0f5e544baa0f5f4fa1


Нужно завершить go routine (и вернуть stdin в исходное состояние) три раза так, чтобы результирующий вывод консоли при вводе 123 был:


Scanning:
123
What was scanned at i=0: "123"
Scanning:
123
What was scanned at i=1: "123"
Scanning:
123
What was scanned at i=2: "123"
эм, а можете словами описать, что вы хотите получить…
а то у вас прям сразу рейс между основным кодом и той горутиной что вы запускаете, как бы не очень понятно какой результат вы ожидаете

Запустить io.Copy, завершить io.Copy. Использовать stdin по другим назначениям (в моём случае Scanf). И так три раза. Scanf должен отработать правильно в трех случаях.

Это файл, скорее всего. Думаю, kay вот на эту проблему налетел: Issue 10001. Тогда Close() может не помочь, POSIX не обещает, что read(2) и close(2) не вызовут гонок, если вызвать из разных потоков. Поллер по ссылке выше, наверное, самое идиоматичное решение — жаль, что из коробки не идёт.

Спасибо автору за перевод, но все же почему нигде нет ссылки на оригинальную статью? Если она все же есть, просьба ткнуть носом)

В блоке под статьёй, между полосой с рейтингом статьи и информацией об авторе. :)

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории