Конечные автоматы против потоков


24

Алан Кокс однажды сказал: «Компьютер - это конечный автомат. Потоки предназначены для людей, которые не могут программировать конечные автоматы».
Поскольку прямое обращение к Алану - это не вариант для меня, я бы предпочел спросить: как можно достичь многопоточности в языке высокого уровня, например Java, используя только один поток и конечный автомат? Например, что делать, если нужно выполнить 2 действия (выполнение вычислений и ввод / вывод) и одно действие можно заблокировать?
Является ли использование только конечного автомата жизнеспособной альтернативой многопоточности в языках высокого уровня?


4
Компьютер также является машиной Тьюринга. Тем не менее, это не обязательно полезно для программирования как машина Тьюринга. Стек на императивных языках - чрезвычайно полезная вещь, а многопоточные программы позволяют хранить несколько стеков в памяти одновременно. Делать то же самое в автомате, конечно, можно, но тем более грязно.
thiton

10
Алан был разработчиком ядра ОС; это был его домиан . Таким образом, его цитата должна быть взята в этом контексте. Он бы программировал «против металла», где есть смысл использовать такую ​​модель. Как только ОС абстрагирует аппаратное обеспечение и его внутренние атрибуты (что «компьютер является конечным автоматом ...»), у вас появляется возможность и выгода, чтобы иметь возможность использовать другие модели, которые имеют больше смысла в вашем домене . Почти каждая игра интенсивно использует конечные автоматы.
Стивен Эверс

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

2
«Но абстракция [thread] предоставляет вам гораздо более простой и менее подверженный ошибкам интерфейс». Это кажется ложным. Количество людей, у которых неправильная защита потоков, указывает на то, что это может привести к ошибкам.
С.Лотт

1
Многие комментарии и ответы здесь интерпретируют цитату как анти-многозадачность в целом; Я полагаю, что Алан Кокс - просто антипоток и будет выступать за использование нескольких процессов для достижения многих целей, для которых люди используют потоки. Имейте в виду, что он хакер Unix: форк FTW. Я не нашел ни одного комментария от него непосредственно в этой цитате, но вот один от Ларри Маквой из списка рассылки ядра Linux, который идет в этом направлении: lkml.indiana.edu/hypermail/linux/kernel/0106.2/0405.html
Мартин Б

Ответы:


25

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

На одноядерном компьютере ничего не происходит "одновременно". Это все просто чередованное исполнение.

Есть много, много способов добиться чередования. Многие.

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

  • T1-до блокировки
  • Т1-с замком
  • T1-после блокировки
  • Т2-до блокировки
  • Т2-с замком
  • T2-после блокировки

[Это может быть в цикле или иметь больше замков или что-то еще. Все, что он делает, это становится длиннее, а не сложнее.]

Шаги 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 будет два варианта (два потока - два варианта), и бросок монеты может определить, за каким конкретным состоянием следует следовать.


Спасибо, хорошее объяснение. А как насчет многоядерной машины? Я предполагаю, что нет явного способа использовать ядра в Java State Machine? Для этого нужно полагаться на ОС, верно?
Виктор Сорокин

5
Многоядерный компьютер делает планирование немного более сложным. Однако все ядра записывают в одну общую память, поэтому порядок записи в память между двумя ядрами означает, что, по сути, мы вернулись к чередующемуся выполнению операций записи в память. ОС использует ядра, а JVM использует это. Не нужно дважды об этом думать.
С.Лотт

9

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

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

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

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


Ошибка кодовой страницы в программе с одним контекстом выполнения по сути блокирует. По определению, код, имеющий единственный контекст выполнения, не может продвигаться вперед, пока не будет доступен следующий фрагмент кода в потоке.
Дэвид Шварц

1
@ Дэвид Шварц: правда, это принципиально блокирует; но это не «операция», так как это не то, что делает заблокированный код, это то, что происходит с ним.
Хавьер

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

1
@ Дэвид Шварц: Определение «блокирующего» поведения всегда подчиняется требованиям «в реальном времени». Например: ошибка кодовой страницы считается неблокируемой для приложения, которому требуется скорость отклика порядка сотен миллисекунд. С другой стороны, если приложение предъявляет строгие требования в реальном времени, оно вообще не будет использовать виртуальную память.
MaR

1
@Mar: ... или используйте детерминированные алгоритмы обмена, которые гарантируют получение необходимых данных, прежде чем это станет необходимым.
SF.

9

Как достичь многопоточной функциональности на языке высокого уровня, таком как Java, используя только один поток и конечный автомат? Например, что делать, если нужно выполнить 2 действия (выполнение вычислений и ввод / вывод) и одно действие можно заблокировать?

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

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

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

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

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

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

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

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

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


2

Что делать, если нужно выполнить 2 действия (выполнение вычислений и ввод / вывод) и одно действие можно заблокировать?

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

Из-за того, что я прочитал оригинальное письмо Кокса (ниже), он отмечает, что эта многопоточность плохо масштабируется Я имею в виду, что если есть 100 запросов ввода / вывода? 1000? 10000? Кокс указывает, что наличие большого количества потоков может привести к серьезным проблемам:

От: Алан Кокс (alan@lxorguk.ukuu.org.uk)
Дата: пт 21 января 2000 - 13:33:52 EST

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

Это меньше всего беспокоит тебя. 1000 потоков - это 8 МБ стеков ядра, и достаточно переключения задач, чтобы быть уверенным, что вы также можете отключить большую часть своего кэша. Компьютер - это конечный автомат. Темы предназначены для людей, которые не умеют программировать конечные автоматы.

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

Алан

Источник: Re: Интересный анализ потоков ядра Linux от IBM (архивы списков рассылки ядра Linux)


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

  • Проблема с государственными машинами - комбинаторный взрыв. Число состояний компьютера с оперативной памятью 4G составляет 2 ^ (2 ^ 32) состояния (не считая диска 2T).

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


1

Потоки - единственный вариант в двух случаях:

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

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

Что касается простоты использования и читабельности, всегда есть циклы событий (например, libev или EventMachine ), которые делают программирование конечного автомата почти таким же простым, как выполнение с потоками, но при этом дают достаточно контроля, чтобы забыть о проблемах синхронизации.


1
Лучшее из двух миров: маленькие блокирующие конечные автоматы делают код приложения очень простым. И потоки хорошо разделяются на ядра, если они у вас есть. Если во всем нет как минимум двух ядер, это скоро произойдет. то есть телефоны на базе четырехъядерных процессоров, приходящие в 2012 году.
Тим Виллискрофт

0

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

while (true) {
    switch (event) {
        case ButtonPressed:
        ...
        case MachineIsBurning:
        ....
    }
}

Теперь довольно быстро становится ясно, что уровень высокоуровневого управления в этой конструкции не может быть высоким: обработчик для ButtonPressed должен завершить работу без взаимодействия с пользователем и вернуться к основному циклу, потому что, если это не так, дальнейших пользовательских событий нет. могут быть обработаны. Если у него есть какое-либо состояние для сохранения, это состояние должно быть сохранено в глобальных или статических переменных, но не в стеке; то есть нормальный поток управления на императивном языке ограничен. Вы по сути ограничены в государственной машине.

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

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

Итак, чтобы ответить на последний вопрос: да, конечный автомат является альтернативой, большинство GUI работают таким образом с потоком GUI. Только не надавливайте конечный автомат слишком быстро, он становится недостижимым очень быстро.


0

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

Абстракция использования многопоточности упрощает реализацию более сложных параллельных систем, но в конечном итоге все параллельные системы сталкиваются с одними и теми же проблемами. Классические проблемы, такие как Deadlock / Livelock и инверсия приоритетов , также возможны для систем на основе конечного автомата, как и для параллельной совместно используемой памяти , системы на основе NUMA или даже CSP , если она достаточно сложна.


0

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

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

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

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

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


2
Конечные автоматы совсем не сложны! Сложные
конечные

2
-1 за "не пытайся". это худший совет, который вы можете дать.
Хавьер

1
-1 «Не пытайся»? Это просто глупо. Я бы также оспорил ваше утверждение о том, что конечные автоматы сложны. Как только вы попадаете в нечто вроде Heirarchal Fuzzy State Machine ... тогда, да, это немного сложнее. Но простой конечный автомат? Это довольно простые вещи, которые я изучал каждый второй год, когда я учился в школе.
Стивен Эверс

позвольте мне перефразировать бит "не пытайся" ...
gbjbaanb

0

Хороший пример правильного использования конечного автомата вместо потоков: nginx vs apache2. Обычно вы можете предположить, что nginx обрабатывает все соединения в одном потоке, apache2 создает поток для каждого соединения.

Но для меня использование конечных автоматов против потоков очень похоже на использование вручную созданного asm vs java: вы можете достичь невероятных результатов, но это требует больших усилий программистов, большой дисциплины, делает проект более сложным и полезным только при использовании много других программистов. Так что, если вы хотите создать быстрый веб-сервер - используйте машины состояний и асинхронный ввод-вывод. Если вы пишете проект (а не библиотеку, которая будет использоваться повсеместно) - используйте потоки.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.