Алан Кокс однажды сказал: «Компьютер - это конечный автомат. Потоки предназначены для людей, которые не могут программировать конечные автоматы».
Поскольку прямое обращение к Алану - это не вариант для меня, я бы предпочел спросить: как можно достичь многопоточности в языке высокого уровня, например Java, используя только один поток и конечный автомат? Например, что делать, если нужно выполнить 2 действия (выполнение вычислений и ввод / вывод) и одно действие можно заблокировать?
Является ли использование только конечного автомата жизнеспособной альтернативой многопоточности в языках высокого уровня?
multithreading
finite-state-machine
Виктор Сорокин
источник
источник
Ответы:
Все, что делает поток, это чередует операции, так что части процесса, кажется, перекрываются во времени. Одноядерный компьютер с несколькими потоками просто прыгает: он выполняет небольшие биты кода из одного потока, а затем переключается на другой поток. Простой планировщик решает, какой поток имеет наивысший приоритет и фактически выполняется в ядре.
На одноядерном компьютере ничего не происходит "одновременно". Это все просто чередованное исполнение.
Есть много, много способов добиться чередования. Многие.
Допустим, у вас есть простой двухпоточный процесс, который использует простую блокировку, чтобы оба потока могли записывать в общую переменную. У вас есть шесть блоков кода.
[Это может быть в цикле или иметь больше замков или что-то еще. Все, что он делает, это становится длиннее, а не сложнее.]
Шаги T1 должны выполняться по порядку (T1-before, T1-with, T1-after), а шаги T2 должны выполняться в порядке (T2-before, T2-with, T2-after).
Помимо ограничения «по порядку», они могут чередоваться любым способом. Так или иначе. Они могут быть запущены, как указано выше. Другой действительный порядок: (T1-before, T2-before, T2-lock, T1-lock, T2-after, T1-after). Есть много действительных заказов.
Подождите.
Это просто конечный автомат с шестью состояниями.
Это недетерминированные конечные автоматы. Порядок состояний T1-xxx с состояниями T2-xxx является неопределенным и не имеет значения. Так что есть места, где «следующий штат» - это подбрасывание монеты.
Например, когда запускается FSM, T1-before или T2-before являются законными первыми состояниями. Бросить монету.
Допустим, до T1 до этого. Сделай это. Когда это сделано, есть выбор между T1-с и T2-до. Бросить монету.
На каждом этапе в FSM будет два варианта (два потока - два варианта), и бросок монеты может определить, за каким конкретным состоянием следует следовать.
источник
Написание функций блокировки для людей, которые не могут создавать конечные автоматы;)
Потоки полезны, если вы не можете обойти блокировку. Никакая фундаментальная компьютерная деятельность действительно не блокирует, просто многие из них реализованы таким образом для простоты использования. Вместо возврата символа или «сбой чтения», функция чтения блокируется, пока не будет прочитан весь буфер. Вместо проверки возврата сообщения в очереди и возврата, если ничего не найдено, функция подключения ожидает ответа.
Вы не можете использовать блокирующие функции в конечном аппарате (по крайней мере, одну, которую нельзя «заморозить»).
И да, использование конечного автомата является жизнеспособной альтернативой. В системах реального времени это единственный вариант, система, обеспечивающая основу для машины. Использование потоков и блокирующих функций - это просто «легкий выход», потому что обычно один вызов блокирующей функции заменяет около 3-4 состояний в конечном автомате.
источник
То, что вы описываете, называется кооперативной многозадачностью , где задачи передаются ЦПУ и ожидаются добровольно, по истечении некоторого времени или активности. Задача, которая не взаимодействует, продолжая использовать ЦП или блокируя гуммирование всей работы и не имея аппаратного сторожевого таймера, код, контролирующий задачи, ничего не может с этим поделать.
То, что вы видите в современных системах, называется упреждающей многозадачностью , когда задачам не нужно освобождать ЦП, потому что супервизор делает это за них, когда прибывает аппаратное прерывание. Процедура обработки прерываний в супервизоре сохраняет состояние ЦП и восстанавливает его в следующий раз, когда задача считается заслуживающей временного интервала, затем восстанавливает состояние из любой задачи, которая должна быть запущена следующей, и возвращается к ней, как будто ничего не произошло , Это действие называется переключением контекста и может быть дорогим.
Жизнеспособное? Конечно. Здравомыслящий? Иногда. Используете ли вы потоки или некоторую форму домашней многозадачной кооперации (например, конечные автоматы), зависит от компромиссов, которые вы готовы сделать.
Потоки упрощают проектирование задач до такой степени, что вы можете рассматривать каждую из них как свою собственную программу, которая может совместно использовать пространство данных с другими. Это дает вам свободу сосредоточиться на текущей работе, а не на всех управленческих и хозяйственных работах, необходимых для того, чтобы заставить ее работать итерацию за раз. Но поскольку ни одно доброе дело не остается безнаказанным, вы платите за все это удобство в переключениях контекста. Наличие множества потоков, которые выдают ЦП после выполнения минимальной работы (добровольно или делая что-то, что блокирует, например, ввод / вывод), может потреблять много процессорного времени при переключении контекста. Это особенно верно, если ваши блокирующие операции редко блокируются очень долго.
В некоторых ситуациях маршрут сотрудничества имеет больше смысла. Однажды мне пришлось написать какое-то пользовательское программное обеспечение для аппаратного обеспечения, которое передавало много каналов данных через отображаемый в памяти интерфейс, который требовал опроса. Каждый канал был объектом, построенным таким образом, что я мог либо позволить ему работать как поток, либо повторно выполнить один цикл опроса.
Производительность многопоточной версии совсем не была хороша именно по той причине, о которой я говорил выше: каждый поток выполнял минимальную работу, а затем выдавал процессор, чтобы другие каналы могли иметь некоторое время, вызывая много переключений контекста. Позволение потокам работать свободно до тех пор, пока выгрузка не будет прервана, что помогло с пропускной способностью, но привело к тому, что некоторые каналы не обслуживались до того, как аппаратное обеспечение испытало переполнение буфера, потому что они не получили достаточный интервал времени.
Однопоточная версия, которая выполняла даже итерации каждого канала, работала как обезжиренная обезьяна, а нагрузка на систему падала как скала. Штраф, который я заплатил за дополнительное выступление, заключался в том, что я сам подтасовывал задачи. В этом случае код для его выполнения был достаточно прост, чтобы затраты на его разработку и поддержку стоили повышения производительности. Я думаю, что это действительно суть. Если бы мои потоки сидели без дела, ожидая возвращения какого-то системного вызова, упражнение, вероятно, не стоило бы того.
Это приводит меня к комментарию Кокса: темы предназначены не только для людей, которые не могут писать конечные автоматы. Некоторые люди вполне способны на это, но предпочитают использовать постоянный конечный автомат (т. Е. Поток) в интересах выполнения работы раньше или с меньшей сложностью.
источник
Ну, я, честно говоря, не представляю, как справиться с блокировкой ввода-вывода без потоков. Это называется блокирование в конце концов только потому, что код, который вызывает его, должен
wait
.Из-за того, что я прочитал оригинальное письмо Кокса (ниже), он отмечает, что эта многопоточность плохо масштабируется Я имею в виду, что если есть 100 запросов ввода / вывода? 1000? 10000? Кокс указывает, что наличие большого количества потоков может привести к серьезным проблемам:
Источник: Re: Интересный анализ потоков ядра Linux от IBM (архивы списков рассылки ядра Linux)
источник
В теории это правда. В реальной жизни потоки - это просто эффективная абстракция, используемая для программирования такого конечного автомата. Они настолько эффективны, что их также можно использовать для программирования диаграмм состояний и сетей Петри (т. Е. Параллельного поведения, когда конечные автоматы в основном последовательны).
Проблема с государственными машинами - комбинаторный взрыв. Число состояний компьютера с оперативной памятью 4G составляет 2 ^ (2 ^ 32) состояния (не считая диска 2T).
Для человека, чей единственный инструмент - молоток, каждая проблема выглядит как гвоздь.
источник
Потоки - единственный вариант в двух случаях:
Во-вторых, большинство людей считают, что потоки неизбежны для ввода-вывода или сетевого программирования, но обычно это происходит потому, что они не знают, что их ОС имеет более продвинутый API (или не хотят бороться с его использованием).
Что касается простоты использования и читабельности, всегда есть циклы событий (например, libev или EventMachine ), которые делают программирование конечного автомата почти таким же простым, как выполнение с потоками, но при этом дают достаточно контроля, чтобы забыть о проблемах синхронизации.
источник
Хороший способ понять, как взаимодействуют конечные автоматы и многопоточность, - взглянуть на обработчики событий графического интерфейса. Многие приложения / каркасы GUI используют один поток GUI, который будет опрашивать возможные источники ввода и вызывать функцию для каждого полученного ввода; по сути, это можно записать как огромный переключатель:
Теперь довольно быстро становится ясно, что уровень высокоуровневого управления в этой конструкции не может быть высоким: обработчик для ButtonPressed должен завершить работу без взаимодействия с пользователем и вернуться к основному циклу, потому что, если это не так, дальнейших пользовательских событий нет. могут быть обработаны. Если у него есть какое-либо состояние для сохранения, это состояние должно быть сохранено в глобальных или статических переменных, но не в стеке; то есть нормальный поток управления на императивном языке ограничен. Вы по сути ограничены в государственной машине.
Это может стать довольно грязным, когда у вас есть вложенные подпрограммы, которые должны сохранять, например, уровень рекурсии. Или находятся в процессе чтения файла, но файл в данный момент недоступен. Или просто в длинных вычислениях. Во всех этих случаях становится желательным сохранить состояние текущего выполнения и вернуться к основному циклу, а это многопоточность . Ни больше ни меньше.
Все это стало немного более запутанным с введением вытесняющей многопоточности (то есть операционная система решает, когда потоки должны давать контроль), и поэтому соединение не сразу ясно сегодня.
Итак, чтобы ответить на последний вопрос: да, конечный автомат является альтернативой, большинство GUI работают таким образом с потоком GUI. Только не надавливайте конечный автомат слишком быстро, он становится недостижимым очень быстро.
источник
Спрашивать, является ли использование конечного автомата жизнеспособным на языке высокого уровня, немного похоже на вопрос, является ли написание на ассемблере жизнеспособной альтернативой использованию языка высокого уровня. У них обоих есть свое место, учитывая правильную ситуацию.
Абстракция использования многопоточности упрощает реализацию более сложных параллельных систем, но в конечном итоге все параллельные системы сталкиваются с одними и теми же проблемами. Классические проблемы, такие как Deadlock / Livelock и инверсия приоритетов , также возможны для систем на основе конечного автомата, как и для параллельной совместно используемой памяти , системы на основе NUMA или даже CSP , если она достаточно сложна.
источник
Я не думаю, что это так - конечно, конечные автоматы - это очень «элегантная» вычислительная концепция, но, как вы говорите, они довольно сложны. И сложные вещи трудно понять правильно. А то, что неправильно, просто сломано, поэтому, если вы не гений предполагаемого роста Алана Кокса, придерживайтесь того, что, как вам известно, работает - оставьте «умное кодирование» учебным проектам.
Вы можете сказать, когда кто-то предпринял тщетную попытку сделать это, поскольку (при условии, что это работает хорошо), когда дело доходит до его поддержания, вы обнаруживаете, что задача почти невозможна. Оригинальный «гений» перестанет оставлять вас с кусочком непонятного кода (поскольку разработчики такого типа не склонны оставлять слишком много комментариев, не говоря уже о технической документации).
В некоторых случаях конечным автоматом будет лучший выбор - сейчас я думаю о встраиваемых типах, где некоторые шаблоны конечных автоматов используются и используются неоднократно и более формализованно (т.е. правильное проектирование :))
Потоки тоже могут быть сложными, но есть шаблоны, которые помогут вам продвинуться вперед - главным образом за счет уменьшения необходимости обмена данными между потоками.
Последнее замечание по этому поводу заключается в том, что современные компьютеры в любом случае работают на многих ядрах, поэтому конечный автомат не будет в полной мере использовать имеющиеся ресурсы. Потоки могут сделать лучшую работу здесь.
источник
Хороший пример правильного использования конечного автомата вместо потоков: nginx vs apache2. Обычно вы можете предположить, что nginx обрабатывает все соединения в одном потоке, apache2 создает поток для каждого соединения.
Но для меня использование конечных автоматов против потоков очень похоже на использование вручную созданного asm vs java: вы можете достичь невероятных результатов, но это требует больших усилий программистов, большой дисциплины, делает проект более сложным и полезным только при использовании много других программистов. Так что, если вы хотите создать быстрый веб-сервер - используйте машины состояний и асинхронный ввод-вывод. Если вы пишете проект (а не библиотеку, которая будет использоваться повсеместно) - используйте потоки.
источник