LINUX.ORG.RU

Как вы пишете асинхронный, интерактивный код?

 


0

3

Имеется привычная ситуация: UI, в котором пользователь бездумно жмакает кнопочки, которые в свою очередь запускают асинхронные/параллельные задачи. Это может быть как и GUI, так и сайт, посылающий запросы серверу.

А теперь начинается интересное:

  1. Любое действие должно быть отменяемым (не убивание потока, а штатное завершение). Большинство async кода что я видел, не поддерживает такую простую фичу. Видимо вебсерверам это не интересно. Даже в Go, с его горутинами, это достигается велосипедами, а не средствами языка.
  2. Любое действие может быть продолжено. Это немного похоже на предыдущую задачу, но с тем нюансом, что у нас задача представляет собой не loop {}, а цепочку действий.
  3. Задача может общаться с родителем. Запрашивать новые данные, подтверждать действия.
  4. Задача может использовать глобальный контекст. То есть живёт не сама по себе.
  5. Задача может запускать свои асинхронные задачи и на неё распространяются те же требования, что и выше.
  6. Это не должно превратиться в callback hell.

Язык роли не играет. Производительность тоже. Интересует сам алгоритм.

Ответ на: комментарий от WitcherGeralt

Это если о нативе говорить.

А что не так с pthread_kill и pthread_cancel?

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

Процессы асинхронные, можно останавливать через kill

sys:suspend/sys:resume, если мы хотим останавливать/продолжать (процесс должен при этом реализовывать стандартные поведения или быть созданным с использованием proc_lib).

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

Килом ты ведь ничего не убиваешь, чтобы нормально завершиться, опять же, понадобится разбросать точки прерывания с флагами. Если я правильно помню. С cancel проблем с памятью не оберёшься.

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

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

В managed языках можно, конечно. Там по сути и так есть и цикл, обрабатывающий фреймы/кванты твоего кода, и флаги.

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

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

Так финализаторы пропиши, когда GC будет мусор собирать, он их вызовет. Без велосипедов и костылей, нахаляву.

Можно, конечно и в нативе всё к какому-то объекту контекста привязывать, а при cancel в обработчике по нему зачищать. Но во-первых, это геморно, а во вторых встаёт вопрос атомарности операций в треде. Проще написать так, чтобы результат работы потока ни на что не влиял, когда тон больше не требуется. @monk

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

Проще написать так, чтобы результат работы потока ни на что не влиял, когда тон больше не требуется

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

нативный тред прибивается так, извне. аксиомы.

  1. пока нативный тред A исполняется процом - его прервать нельзя. поскольку он занимает проц в данный момент и прерываться не собирается.
  2. как только его квант времени закончился, шедулер переключается на другой тред B(тредов больше чем ядер и они реально вытесняются), допустим этот В хочет прибить A. тогда он говорит шедулеру что-то типа stop_thread(A). тогда шедулер удаляет A из очереди активных, и выполняется код утилизации ресурсов треда A(или задача утилизации ресурсов A кладетсяd в очередь задач спец треда-чичтильщика, что асинхронно утилизирует различные системные обьекты), утилизация там примерно такова - отдача стека в кучу, отпускание мьютексов, если они были захвачены тредом A и все такое. никакие файлы, пайпы, сокеты и проч не закрываются, потому что никто не знает что тред их вообще открывал. это знает процесс, но процесс активен и его никто не прибивал.

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

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

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

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

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

pthread_kill

не поможет освободить данные, которые обрабатывал этот тред. Это главная проблема такого dos-style управления ресурсами.

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

Килом ты ведь ничего не убиваешь, чтобы нормально завершиться, опять же, понадобится разбросать точки прерывания с флагами

Там же сигнал. Как захочешь, так и обработаешь. Можно просто закрыть все ресурсы и pthread_exit сделать.

С cancel проблем с памятью не оберёшься.

Там же можно очистку прикрутить.

Вот хорошее описание: https://stackoverflow.com/questions/2084830/kill-thread-in-pthread-library/6560246#6560246

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

невозможно корректным образом автоматически «разбить» код на «целостные кванты» между которыми прерывание треда будет корректным.

Вот поэтому в Racket и Erlang зелёные треды. Для них такая задача решена.

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

Это главная проблема такого dos-style управления ресурсами.

причем тут dos style. никакой не-дос style не решит задачи корректного прерывания треда извне, если он допустим апгрейдит ваше железо. в любом случае, прервав это посредине, вы получите кирпич.

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

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

Вот поэтому в Racket и Erlang зелёные треды. Для них такая задача решена.

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

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

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

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

https://docs.racket-lang.org/foreign/Atomic_Execution.html

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

monk ★★★★★ ()

Я конечно тупень, но у меня решение в голове представляется как event_queue, который по-очереди выделяет ресурс на обработку событий. Для описания событий, вот всех этих отмен и прочего сделал бы (на java) абстрактный класс, наследованный от Runnable. Далее оформил конкретную реализацию класса с поступенчатым выполнением, отладил. Сразу бы сделал ThreadPool из своих классов, чтобы быстрее можно было генерировать события. Контекст можно сохранять в классе события, но сделать ссылку на него мьютексом.

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

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

Если надо только указать запрет на остановку через break-thread, а не полностью остановить остальную программу, то есть parameterize-break и перехват исключений.

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

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

Atomic mode evaluates a Racket expression without switching among Racket threads and with limited support for events. An atomic computation in this sense is not atomic with respect to other places, but only to other threads within a place.

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

а мы толкуем про убийство треда извне.

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

Так в критической секции «извне» будет ждать, пока критическая секция закончится.

А если нет (не через atomic, а через parameterize-break), то убийство треда через break-thread корректно обрабатывается внутри треда (break будет получен после окончания критической секции).

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

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

В Racket зелёные треды и свой шедулер.

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

если б я вел собеседование, то задал бы вопрос.

тред А захватил три мьютекса и сказал sleep(100 секунд). как его корректно удалить из системы. псевдокод или вольное изложение.

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

В линуксе, точнее в glibc, канцеляция потока приводит к исключению https://udrepper.livejournal.com/21541.html. Потоки не канцеляются в произвольных точках «посреди выражения», а только в точках канцеляции, которыми являются некоторые вызовы posix. Если все ресурсы обернуты в raii врапперы, исключение откатит стек, врапперы освободят ресурсы.

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

Потоки не канцеляются в произвольных точках «посреди выражения», а только в точках канцеляции, которыми являются некоторые вызовы posix.

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

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

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

Надо делать как в glibc. При канцеляции потока в нём из точки канцеляции вылетает исключение и откатывает стек. Все ресурсы должны быть в raii обертках на стеке.

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

опять же стоп. получается тред B попросил прибить тред А, при этом в треде А просто взвелся внутренний флаг, типа pending_stop, и как только вызвали какие-то системные функции, там флаг проверили и тред прибили… толк с этого есть, но небольшой. по крайней мере прибили его не в случайной точке, но системный вызов как точка уничтожения - не очень. опять же, если тред ничего такого не вызывает - его не уничтожить.

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

При канцеляции потока в нём из точки канцеляции вылетает исключение и откатывает стек. Все ресурсы должны быть в raii обертках на стеке.

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

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

Весь код должен быть exception-safe, тогда при канцеляции ресурсы не утекают. Но это относится не только к канцеляции, но и к любым исключениям. Если мьютекс захвачен без raii обертки, то вылет любого исключения приведет к утечке.

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

В Go для этого используют Context. Просто себе делаешь обвертку какую-то и в остальном коде приложения можешь проще использовать.

https://golang.org/pkg/context/#WithCancel
Для этой функции есть пример в доке.

urxvt ★★★★★ ()

Тебе надо или язык где это из коробки или самому сделать, думаю ничего сложного. Ну и надо смотреть еще какие «задачи».

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

Весь код должен быть exception-safe, тогда при канцеляции ресурсы не утекают.

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

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

Нужно подучить Go чтобы понимать что тут происходит.

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

При канцеляции потока в нём из точки канцеляции вылетает исключение и откатывает стек. Все ресурсы должны быть в raii обертках на стеке.

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

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

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

Это выше уже даже обсудили.

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

Ну там суть достаточно простая: передается примитив для синхронизации (канал в нашем случае, condition в Python) и по нему периодически проверяется выставил ли пользователь запрос на отмену с помощью этого флага.
В Java похоже сделано — такой флаг есть у каждого потока.

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

Общая суть-то понятна. Но в этом пример слишком много go-специфичных конструкций.

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

Если на Си, надо вешать обработчик на sigaction, освобождающий мьютексы.

Если на Racket, то всё тривиально:

(define sema1 (make-semaphore 1))
(define sema2 (make-semaphore 1))
(define sema3 (make-semaphore 1))

(define thd-A 
  (thread
    (λ()
      (dynamic-wind 
        (λ()
          (parameterize-break #f
            (semaphore-wait sema1) ;; захватываем мьютексы
            (semaphore-wait sema2) 
            (semaphore-wait sema3)))
        (λ() (sleep 100))
        (λ()
          (parameterize-break #f
            (semaphore-post sema1) ;; освобождаем мьютексы
            (semaphore-post sema2)
            (semaphore-post sema3)))))))

(sleep 5)
(break-thread-A thd) ;; все мьютексы освободятся по факту выхода из dynamic-wind
monk ★★★★★ ()
Ответ на: комментарий от alysnix

а ты кстати понял, что теперь любой код, поскольку он всегда исполняется каком-то тредом процесса, придется писать с обертками

Так и делается. На C++ любая строка может вызвать исключение, а значит весь код пишется так, чтобы исключение в любой строке не привело к утечке ресурсов. RAII для этого и придумали.

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

Если на Си, надо вешать обработчик на sigaction, освобождающий мьютексы.

а откуда вы знаете, какие мьютексы захватил ваш код??? почему вы их явно написали в одном месте, неужели непонятно, что это не тот случай.

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

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

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

В случае Racket любой семафор захватывается или через dynamic-wind или через call-with-semaphore (который внутри тот же dynamic-wind). Поэтому неважно где он захвачен, при прерывании потока всё нормально закроется. В C++ с нормальным использованием RAII, аналогично. Думаю В java с try/finally и thread.interrupt тоже всё корректно работает.

а откуда вы знаете, какие мьютексы захватил ваш код???

Если очень хочется, то можно сделать свою версию pthread_mutex_lock, которая будет записывать свои вызовы. Но всё равно большинство сишных библиотек пишутся в предположении, что если вычисление прервалось в критической секции, то при попытке повторного запуска поведение неопределено. Согласен, что убивать сишные потоки — плохая идея.

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

В случае Racket любой семафор захватывается или через dynamic-wind или через call-with-semaphore (который внутри тот же dynamic-wind). Поэтому неважно где он захвачен, при прерывании потока всё нормально закроется.

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

простой пример. чисто модельный. один тред пишет в список пути файлов, что надо удалить. другой тред получает эти пути, удаляет файлы или диры. ты взял и красиво снаружи прихлопнул первый тред. и второй вместо пути к дире получил недоформированный путь прям к твоей хоум дире. поскольку первый тред просто не успел приписать к ней правильный хвост. второй тред удалил весь твой хоум…зато ты своими «dynamic-wind» обеспечил «правильную» отдачу мьютексов. Может она и правильная была, только ты остался без свой хоум диры… вывод - никакая «правильная» отдача и возврат ресурсов не предохраняют он неприятных неожиданностей. пример чисто модельный, поскольку такого рода последствия могут наступить не в результате прерывания такого простейшего действа как формирование нужного пути. тут вопрос просто в фантазии.

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

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

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

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

В Go наоборот весь код невидимо async/await. Единственный язык который такое делает

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

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

Я вот все сижу и жду, когда кто-нить произнесет слово - транзакция.

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

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

В Go наоборот весь код невидимо async/await. Единственный язык который такое делает

жесть… там x = y+z, тоже вычисляются асинхронно с эвэйтом? я ж говорю жесть!

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

быть не может. невозможно корректным образом автоматически «разбить» код на «целостные кванты» между которыми прерывание треда будет корректным

Это именно то что делает Эрланг, только он использует свои «треды» которые называются процессами. И они не совсем «зеленые» как в других языках в том смысле что обычно зеленые треды исполняются в одном процессе ОС но в Эрланге они раскидываются на разные процессоры.

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

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

Видишь в чем прикол в том же Эрланге нету мьютексов, во всяком случае на пользовательском уровне.

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

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

Ещё раз. Либо список передаётся второму потоку по факту формирования и второй поток ничего не получит, либо он формируется внутри того же dynamic-wind и в финализаторе либо завершается, либо очищается.

А транзакции — это, конечно, хорошо. Но встречаются реально они только в СУБД. Если не считать упомянутого atomic, который обеспечит транзакцию ценой остановки всей остальной программы.

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

Но похоже вы доказали их ненужность.

Транзакция — это очень дорого. Попробуйте написать хотя бы транзакционную версию fprintf. Или qsort.

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

monk ★★★★★ ()
Ограничение на отправку комментариев: только для зарегистрированных пользователей