LINUX.ORG.RU

Как вежливо убить linux-тред по таймауту?

 , ,


0

1

Такой языко-агностичный вопрос:

Нужно запустить некий кусок вычислений в параллель (создать поток для него) и убить этот кусок вычислений через N_EXEC_TIMEOUT секунд. Но убить «грациозно», отправив некий сигнал потоку, выполняющему вычисления.

Вопрос в том, что поток не может обрабатывать сигнал (ALRM, например) монопольно, ведь таких кусков вычислений в один и тот же момент времени может быть сколько угодно.

Правильно ли я понимаю, что pthread_kill каким-то чудесным образом решает эту проблему (корень которой в том, что обработчики сигналов ставятся на уровне процесса, а на уровне потока можно только замаскировать получение сигналов)?

Есть ли в реалтаймовых расширениях для ядра Linux некий механизм, позволяющий ограничивать время работы потока? Что-то вроде «суммарное время исполнения кода этого потока не может превышать 500 ms, если больше - сначала запусти предопределённый soft-обработчик, определённый в коде потока, а если нет такого или после запуска soft-обработчика поток добрался до верхней hard-границы времени исполнения, то просто освободить все ресурсы потока и убить его»?

В итоге это всё нужно для того, чтобы можно было делать что-то вроде:

# async, "green" thread
spawn do
  # 2 pthreads will be created:
  # 1-st is a watchdog thread to break our calculations on timeout
  # 2-nd - for calculations itself
  # 1-st is watching for 2-nd thread status. It will terminate if 2-nd thread exited OR timeout occured
  # our async/"green" thread will yield/block while joining the 2-nd thread
  result = with_timeout do
    ... some code ...
  end
rescue Exception::Timeout
  puts "Timeout occured"
end
★★★★★

Последнее исправление: DRVTiny (всего исправлений: 1)

При убивании потока он должен оставить память (и другие ресурсы) в согласованном состоянии. Поэтому убиться он может только сам, а ты можешь только сообщить ему об этом любым образом (например, выставив флаг, который твой поток будет проверять периодически, если поток может зависнуть в блокирующем системном вызове, то его нужно разбудить сигналом, при этом поток должен проверять на EINTR).

anonymous
()
Ответ на: комментарий от anonymous

Если поток поймает сигнал, что-то он сможет сделать, хотя конечно обработчики сигналов - не лучшее место для того, чтобы делать что бы то ни было.

Для очистки памяти, возможно, было бы круто заключить следующее соглашение с вычислительным кодом потока: он должен использовать только стек и/или только выделенный heap определённого размера, выделенный специально для него.

Этого довольно просто добиться в случае кода, написанного в корректном функциональном стиле, когда функция не даёт side-effect'ов.

DRVTiny ★★★★★
() автор топика
Ответ на: комментарий от DRVTiny

Кстати, по смыслу Треды и таймауты - действительно предельно близкая тема, хотя там всё-таки речь о вводе-выводе, который логичнее делать через select/epoll и цикл событий.

DRVTiny ★★★★★
() автор топика
Ответ на: комментарий от anonymous

если поток может зависнуть в блокирующем системном вызове, то его нужно разбудить сигналом, при этом поток должен проверять на EINTR

Очень дельная мысль.

DRVTiny ★★★★★
() автор топика
Ответ на: комментарий от DRVTiny

Если поток поймает сигнал, что-то он сможет сделать, хотя

Суть посылки сигнала потоку не в том, чтобы его обработать, а в том, чтобы поток вышел из блокирующего системного вызова, например, read().

anonymous
()
Ответ на: комментарий от DRVTiny

Для очистки памяти, возможно, было бы круто заключить следующее соглашение с вычислительным кодом потока: он должен использовать только стек и/или только выделенный heap определённого размера, выделенный специально для него.

Поток открыл файловый дескриптор, положил его в стек и тут пришел pthread_cancel()...

anonymous
()
Ответ на: комментарий от DRVTiny

Этого довольно просто добиться в случае кода, написанного в корректном функциональном стиле, когда функция не даёт side-effect'ов.

Функциональный стиль - узконишевое решение. Хорошо подходит для решения специально подобранных задач, на которых детишкам в ВУЗе рассказывают про функциональное программирование. В реальных задачах функциональный стиль - набор жутких ограничений и костылей для их обхода, т.е. не работает совершенно (любой другой вариант будет удобнее).

Но, пока фанаты фукнциональщины со мной не работают и меня это не касается - делай что хочешь. Однако, IMHO, твое решение в функциональном стиле со временем превратиться в ядро, которое решает только те задачи, которые хорошо ложатся на функциональный стиль и обертка, в которую вынесено все, что функциональщиной нормально не решается. При этом весь внешний мир будет общаться с ядром через обертку. Ну, и если выкинуть ядро и все делать без функциональщины, то решение станет проще и удобнее...

anonymous
()
Ответ на: комментарий от DRVTiny

в корректном функциональном стиле,

Может ты еще и думаешь, что функциональный стиль хорошо подходит для многопоточного программирования.

P.S. Кстати, твой код из потока должен будет возвращать значение, т.е. что-то модифицировать, а это уже не функциональный стиль без побочных эффектов.

anonymous
()

Обычный kill делает сигнал, который ядро доставляет произвольному потоку процесса (из тех, в которых этот сигнал не замаскирован). Pthread_kill создает сигнал, который ядро доставляет только в указанный поток. Так что pthread_kill можно использовать для вышивания потока из блокирующегося системного вызова.

В принципе обработчик сигнала может атомарно взводить thread-local флаг, код потока может атомарно проверять и сбрасывать этот флаг перед и после входа в системный вызов.

iliyap ★★★★★
()

Единственный правильный вариант: надо попросить поток остановиться. Как именно это сделать зависит от языка и от твоего приложения. Например это может быть флаг в общей памяти, который поток будет периодически проверять. Убивать поток нежелательно, т.к. от него останется неосвобождённая память, незакрытые дескрипторы и тд. Это делается только в случае, когда альтернатива ещё хуже.

Legioner ★★★★★
()
Ответ на: комментарий от anonymous

Интересно как раз то, как не внося никаких правок в сами вычисления - иметь возможность прервать поток корректно, т.е. так, чтобы сам поток освободил все свои ресурсы.

Для вывода потока из состояния блокировки при системном вызове - можно отправить сигнал.

Ну так а что мешает то же самое сделать и в том случае, если поток просто считает что-то слишком долго? Тоже отправить ему сигнал - и пусть освобождает всё, что нужно.

DRVTiny ★★★★★
() автор топика
Ответ на: комментарий от Legioner

По сути нужно внутри кода потока извне вызвать исключение времени выполнения. Вот как это сделать технически - большой вопрос. Я лично против включения этой логики в сам код потока по ряду причин, включая и ту, что разработчик этого кода может попросту ошибиться с местом вставки проверки флага «пора на боковую». А самое главное - это нивелирует сам подход, потому что с таким же успехом можно в коде вычислительного цикла проверять сразу и время исполнения, зачем нам тогда вообще потоки?

DRVTiny ★★★★★
() автор топика
Ответ на: комментарий от DRVTiny

По сути нужно внутри кода потока извне вызвать исключение времени выполнения. Вот как это сделать технически - большой вопрос. Я лично против включения этой логики в сам код потока по ряду причин, включая и ту, что разработчик этого кода может попросту ошибиться с местом вставки проверки флага «пора на боковую». А самое главное - это нивелирует сам подход, потому что с таким же успехом можно в коде вычислительного цикла проверять сразу и время исполнения, зачем нам тогда вообще потоки?

Это абсолютно нормальный подход, который все используют, ничего такого тут нет. Как ты можешь внутри кода потока вызвать какое-то исключение? Там процессор молотит. Тут есть два варианта. Или у тебя поток висит на вводе/выводе, тогда ты можешь сделать универсальный код и написать свой код ввода/вывода, который будет кидать исключение при interrupt-е (например примерно так сделано в Java), но если у тебя поток не висит на вводе/выводе, то это не поможет; либо ты можешь написать свой компилятор, который помимо прочего ещё будет эту проверку добавлять во все циклы, но суть останется та же, установка флага извне и периодическая проверка этого флага.

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

Legioner ★★★★★
()
Последнее исправление: Legioner (всего исправлений: 1)

В твоём случае я бы подумал о запуске алгоритма во внешнем процессе. Если твой язык совместим с fork/exec, то это может выглядеть вполне естественно и элегантно (если не надо запускать в Windows). В отличие от потоков, процессы убивать можно и ОС за ними обычно практически всё подчищает. Только результат вычислений надо будет через что-то вроде stdout вернуть.

Legioner ★★★★★
()
Последнее исправление: Legioner (всего исправлений: 1)
Ответ на: комментарий от Legioner

В твоём случае я бы подумал о запуске алгоритма во внешнем процессе.

Кстати, тут как раз свежая новость о новом релизе тру-способа остановки длительных дорогостоящих распределённых вычислений с возможностью возобновления(!): CRIU 3.11:

6 ноября вышла новая версия CRIU (Checkpoint and Restore In Userspace). Это проект по разработке инструментария для ОС, основанных на ядре Linux, который позволяет сохранить состояние процесса или группы процессов в файлы на диске и позднее восстановить его, в том числе после перезагрузки системы или на другом сервере без разрыва уже установленных сетевых соединений.

gag ★★★★★
()
Ответ на: комментарий от Legioner

Эврика!!! Если в приложении только main_thread и потоки, которые нужно прервать по таймауту, то можно fork'нуть само приложении по таймауту, а предок сам себя остановит (со всеми вычислительными потоками).

Еще можно в обработчике сигнала таймаута поменять доступ к памяти потока на RWX, и записать в область соответствующую IP потока код, выполняющий throw Exception() и вернуть права на память в RX.

anonymous
()
Ответ на: комментарий от DRVTiny

А самое главное - это нивелирует сам подход, потому что с таким же успехом можно в коде вычислительного цикла проверять сразу и время исполнения, зачем нам тогда вообще потоки?

Просто выставляешь флаг и если грейсфул шатдауна не было, то убиваешь его уже принудительно.

xpahos ★★★★★
()
Ответ на: комментарий от DRVTiny

Интересно как раз то, как не внося никаких правок в сами вычисления - иметь возможность прервать поток корректно, т.е. так, чтобы сам поток освободил все свои ресурсы.

Принципиальная возможность «вежливого» грохания потока должна быть вшита в соответствующую runtime-среду используемого языка программирования - в первую очередь здесь всё упирается в динамические запросы памяти и совместное использование этой памяти разными потоками. Плюс, есть еще всякие прочие дела типа критических секций, атомарных операций и пр... Даже в Java просто убивать поток не рекомендуется.

Так что, в общем случае, поток должен сам отвечать за своё завершение. А если в каком-то языке потоки можно безболезненно «грохать», то об этом будет сказано в соответствующей документации.

vinvlad ★★
()

Ты должен средствами обычного межпоточного взаимодействия отправить потоку сообщение «закругляйся», а в потоке должен быть event loop или расставленные контрольные точки. Все остальные способы не гарантируют корректного завершения с освобождением ресурсов

annulen ★★★★★
()
Ответ на: комментарий от Legioner

В отличие от потоков, процессы убивать можно и ОС за ними обычно практически всё подчищает

Далеко не все. Останутся временные файлы, которым заранее не сделали unlink, а так же файловые локи, именованные семафоры, unix-сокеты и другие ресурсы

annulen ★★★★★
()
Ответ на: комментарий от Legioner

Хотя это конечно намного лучше попыток остановить поток народными средствами

annulen ★★★★★
()
Ответ на: комментарий от DRVTiny

glibc выполняет stack unwinding в потоке, получившем cancellation request. Для C++ это выглядит как исключение abi::__forced_unwind. Так что если поток exception safe, все ресурсы в raii врапперах, то такой код корректно переживет cancellation request.

iliyap ★★★★★
()
Ответ на: комментарий от Legioner

Как ты можешь внутри кода потока вызвать какое-то исключение?

Передачей управления планировщиком ОС не на следующую инструкцию, а на процедуру, у которой в стек засунут rip с тем адресом, который был бы следующим, если бы «не прерывание репортажа».

У молотящего процессора прекрасно отбирает управление прерывание таймера (по логике аппаратные прерывания других типов тоже могут, но тут бить об заклад не буду), по которому переключаются задачи, и ничего не мешает планировщику вернуть управление на какой угодно кусок кода, что, собственно, и происходит при передаче процессу сигнала. Это не магия какая-то, сигналы эти. Это обычный программный механизм: в случае получения сигнала процессом ему повышается приоритет и как только очередь дойдёт до переключения на этот процесс - следующей инструкцией будет уже обработчик сигнала, что достигается нехитрыми манипуляциями с дескриптором задачи/потока и её стеком.

DRVTiny ★★★★★
() автор топика
Ответ на: комментарий от annulen

Так чем отправка сигнала-то не гарантирует?

Ну т.е. можно же вести список открытых fd и всего прочего, что нужно будет освободить при получении сигнала - и освобождать в обработчике сигнала. Разве нет?

DRVTiny ★★★★★
() автор топика
Ответ на: комментарий от DRVTiny

POSIX-сигнала? Они асинхронно обрабатываются, т.е. поток останавливает работу в произвольном месте, а потом продолжает, и в них очень много чего делать просто запрещено (см. ман)

annulen ★★★★★
()
Ответ на: комментарий от annulen

Механизм, как обрабатывается сигнал - очевиден. Ни один запрет не может быть просто запретом. В данном случае я не вижу логичного обоснования того, почему нельзя в обработчике сигнала делать что-то сложнее x = y + z

DRVTiny ★★★★★
() автор топика
Ответ на: комментарий от annulen

Собственно:

On Linux, cancellation is implemented using signals. Under the NPTL threading implementation, the first real-time signal (i.e., signal 32) is used for this purpose. On LinuxThreads, the second real-time signal is used, if real-time signals are available, otherwise SIGUSR2 is used.

(http://man7.org/linux/man-pages/man3/pthread_cancel.3.html)

Внезапно, да?

DRVTiny ★★★★★
() автор топика
Последнее исправление: DRVTiny (всего исправлений: 1)
Ответ на: комментарий от DRVTiny

Так и pthread_cancel не стоит использовать в сложных проектах

annulen ★★★★★
()

# async, «green» thread

Какой же это зелёный тред, если у него стейт в ядре есть?

Запускай вычисления в отдельном процессе, нужную память можно шаренно заммапить во всех. Процесс легко можно грохнуть и ничего из ресурсов не потерять. Чтобы самому белиберду всякую не писать, можно что-то типа HTCondor использовать.

mv ★★★★★
()
Ответ на: комментарий от DRVTiny

On Linux, cancellation is implemented using signals. Under the NPTL threading implementation, the first real-time signal (i.e., signal 32) is used for this purpose. On LinuxThreads, the second real-time signal is used, if real-time signals are available, otherwise SIGUSR2 is used. ...

(http://man7.org/linux/man-pages/man3/pthread_cancel.3.html)

Внезапно, да?

Тут важно понимать, что одного pthread_cancel совсем недостаточно. Для начала, потоку при старте нужно зарегистрировать обработчик соответствующего события (см. pthread_cleanup_push). Далее, режим «грохания» (см. pthread_setcanceltype) должен быть PTHREAD_CANCEL_DEFERRED («A cancellation request is deferred until the thread next calls a function that is a cancellation point»). Тогда реальное прерывание работы потока и передача управления на соответствующий обработчик происходит не в произвольный момент времени, а «безопасно» - при очередном обращении к одной из функций, относящихся к разряду «cancellation point». Если поток, к примеру выполняет какой-то долгий расчет, то он должен периодически «искуственно» выполнять вызов pthread_testcancel(), чтобы его можно было реально прервать.

Вот тогда, в cancel-обработчике можно аккуратно подчистить все ресурсы и завершить работу потока (к примеру, выбросить немаскируемое «спец-исключение», чтобы всё отмоталось автоматом). Причем, если речь идет не о простой Си-шечке, то все эти вызовы и cancel-обработчик должны быть упрятаны в runtime-среду соответствующего языка - «наружу» вам будут выданы соответствующие «обертки» необходимых функций, чтобы вы сами чего не накосячили.

vinvlad ★★
()
Ответ на: комментарий от vinvlad

Уфф. А где можно посмотреть готовую корректную реализацию принудительного внешнего завершения исполнения куска кода по timeout'у?

По мне так на подобный кусок кода просто должны накладываться разумные ограничения: не открывать новые дескрипторы, не создавать из него потоки/процессы и прочее подобное. Это куда лучше, чем мучаться с малоактуальным в реальной практике вопросом: как правильно завершить ВЫЧИСЛЯЮЩИЙ код. Вычисляющему коду достаточно куска памяти, который будет free при cleenup'е целиком, swap'а, который будет также уничтожен - и процессорного времени (которое ограничено).

DRVTiny ★★★★★
() автор топика
Ответ на: комментарий от DRVTiny

В данном случае я не вижу логичного обоснования того, почему нельзя в обработчике сигнала делать что-то сложнее x = y + z

Правда не видишь? Тогда, если не можешь уйти из программирования, по крайней мере вынес свои долгие вычисления в отдельный процесс.

tailgunner ★★★★★
()
Последнее исправление: tailgunner (всего исправлений: 1)
Ответ на: комментарий от DRVTiny

А где можно посмотреть готовую корректную реализацию принудительного внешнего завершения исполнения куска кода по timeout'у?

Понимаешь... Тема у тебя называется «Как вежливо убить linux-тред по таймауту?»... В стартовом топике у тебя присутствует строчка комментариев 'async, «green» thread"'... Ты задаешь вопрос не указав язык программирования, который ты собираешься использовать... О чем это всё говорит?...

Тебе надо подтягивать базовые знания по программированию - даже для чисто прикладного уровня.

vinvlad ★★
()
Ответ на: комментарий от DRVTiny

Интересно как раз то, как не внося никаких правок в сами вычисления - иметь возможность прервать поток корректно, т.е. так, чтобы сам поток освободил все свои ресурсы.

Запустить процесс с MAP_SHARED для общения, по таймауту убивать весь процесс. Должно подойти, разве что найдешь причину зациклиться именно на тредах.

anonymous
()
Ответ на: комментарий от vinvlad

Причём тут язык программирования и всё остальное? Если сказать нечего - можно просто не отвечать, что за рашко-быдловатая манера хамить-то сразу?

Язык программирования - любой, потому что мне интересна внутренняя реализация, а не то, как с помощью магии, костылей и палок это выглядит для конечного пользователя рантайма какой-нибудь Скалы, Эрланга или Эликсира.

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

ОК, давайте на ассемблере. На ассемблере осилите?

P.S. И да, вы либо очень наивный человек, либо слегка оторваны от жизни, если считаете, что хотя бы у 10% обезьянокодеров есть хотя бы 5% моих знаний об архитектуре операционных систем.

DRVTiny ★★★★★
() автор топика
Последнее исправление: DRVTiny (всего исправлений: 3)
Ответ на: комментарий от MKuznetsov

Идея здравая, но зачем это нужно, если можно просто следовать соглашениям?

Если обвязка, обеспечивающая аварийное завершение «залипшего» вычислительного кода, используется для запуска чего-то, что не следует соглашениям, и по этой причине приложение падает - это не проблема того, кто пишет библиотечную обвязку, это проблема разработчика, не следующего указаниям в документации библиотеки.

DRVTiny ★★★★★
() автор топика
Ответ на: комментарий от DRVTiny

Язык программирования - любой, потому что мне интересна внутренняя реализация, а не то, как ...это выглядит для конечного пользователя ... Вопрос вообще не о языках, ...вопрос сугубо о том, как с точки зрения общесистемной применительно конкретно к ядру Linux решить эту задачу.

Так вам выдали всю необходимую информацию - куда уж подробней-то?! А вы в ответ «а где можно посмотреть готовую корректную реализацию»... Вы что, читать документацию и Гуглом пользоваться не умеете? Или так сложно самому набросать простенький пример и потестить его? Ну так тогда вам рановато еще других программеров «обезьянокодерами» называть...

Так что, без обид.

vinvlad ★★
()
Ответ на: комментарий от DRVTiny

Если ваш вычислятор в принципе не будет использовать динамические запросы памяти и прочие сложности - один гольный стек, - то тогда можете и дальше циклиться на потоках. Но поскольку это весьма сомнительный случай и вам, судя по разговору, на самом деле хочется не «вежливо» завершить вычислятор, а просто его «грохнуть», то вам уже несколько раз дали на этот счет конкретный, практичный совет - порождать для «вычислятора» отдельный процесс (наследующий память родителя в режиме сopy-on-write). Результаты возвращать через предоставленную общую память (которая «IPC shared memory») или, там, через какой-нибудь временный файл, создаваемый родителем - с соответствующим контролем целостности, чтобы родитель не поимел потом каких-нибудь проблем. Вот для такого режима взаимодействия ядро Linux-а действительно предоставляет всё необходимое для полноценной подчистки ресурсов «грохнутого» вычислятора. И не нужно будет заморачиваться со всякими системными сложностями.

vinvlad ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.