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

Я изучал изучение Эрланга и в результате читал (хорошо, бегло) о модели актера.

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

Это кажется достаточно тривиальной реализации на C++ или любом другом языке:

class BaseActor {
    std::queue<BaseMessage*> messages;
    CriticalSection messagecs;
    BaseMessage* Pop();
public:
    void Push(BaseMessage* message)
    {
        auto scopedlock = messagecs.AquireScopedLock();
        messagecs.push(message);
    }
    virtual void ActorFn() = 0;
    virtual ~BaseActor() {} = 0;
}

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

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

есть ли что-то еще, чего мне не хватает, или модель действительно так очевидна?

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

6 ответов


код C++ не имеет дело с справедливостью, изоляцией, обнаружением ошибок или распределением, которые являются всеми вещами, которые Erlang приносит как часть своей модели актора.

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

также эмулятор beam SMP приносит JIT-планирование актеров, перемещая их в ядро, которое на данный момент является наименее используемым, а также спящий режим потоков на определенных ядрах, если они больше не нужны.

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

эти вещи не невозможно сделать в C++, но они становятся все труднее, если добавить тот факт, что Erlang работает почти на всех основных конфигурациях hw и os.

edit: только что нашел описание по Wiger УНЧ о том, что он видит параллелизм стиля erlang.


Я не люблю цитировать себя, но из Virding по программированию

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

относительно Гринспана. У Джо (Армстронга) есть аналогичное правило.

проблема не в том, чтобы реализовать актеров, это не так сложно. Проблема в том, чтобы получить все совместная работа: процессы, коммуникация, сбор мусора, языковые примитивы ,обработка ошибок и т. д... Например, использование потоков ОС плохо масштабируется, поэтому вам нужно сделать это самостоятельно. Это было бы похоже на попытку "продать" язык OO, где вы можете иметь только 1K объектов, и они тяжелы для создания и использования. С нашей точки зрения, параллелизм является основной абстракцией для структурирования приложений.

увлекся, поэтому я остановлюсь здесь.


Это на самом деле отличный вопрос, и получил отличные ответы, которые, может быть неубедительной.

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

во-первых, он забирает замки. В книге Джо Армстронга излагается такой мысленный эксперимент: Предположим, ваш процесс приобретает блокировка, а затем немедленно аварийно завершает работу (сбой памяти приводит к сбою процесса или сбою питания в части системы). В следующий раз, когда процесс ждет той же блокировки, система просто заблокирована. Это может быть очевидная блокировка, как в вызове AquireScopedLock() в примере кода; или это может быть неявная блокировка, полученная от вашего имени менеджером памяти, скажем, при вызове malloc () или free ().

в любом случае, ваш сбой процесса теперь остановил всю систему от создания достижения. Фини. Конец истории. Ваша система мертва. Если вы не можете гарантировать, что каждая библиотека, используемая в C/C++, никогда не вызывает malloc и никогда не получает блокировку, ваша система не является отказоустойчивой. Системы Erlang могут и убивают процессы по желанию, когда под большой нагрузкой, чтобы добиться прогресса, поэтому в масштабе ваши процессы Erlang должны быть убиваемыми (в любой точке выполнения) для поддержания пропускной способности.

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

во-вторых, Erlang отнимает статическую типизацию, что, в свою очередь, позволяет обмениваться горячим кодом и запускать две версии одного и того же кода одновременно. Это означает, что вы можете обновить ваш код во время выполнения, без остановки система. Это как системы остаются вверх на 9 9's или 32 msec времени простоя / года. Они просто модернизированы на месте. Ваши функции C++ должны быть вручную повторно связаны для обновления, и запуск двух версий одновременно не поддерживается. Обновление кода требует простоя системы, и если у вас есть большой кластер, который не может запускать более одной версии кода одновременно, вам нужно будет сразу удалить весь кластер. Ай. А в мире телекоммуникаций-нет. терпимый.

кроме того, Erlang забирает общую память и общую общую сборку мусора; каждый легкий процесс-это мусор, собранный независимо. Это простое расширение первой точки, но подчеркивает, что для истинной отказоустойчивости вам нужны процессы, которые не взаимосвязаны с точки зрения зависимостей. Это означает, что ваши паузы GC по сравнению с java терпимы (небольшие, а не паузы на полчаса для завершения GC 8GB) для больших систем.


существуют фактические библиотеки актеров для C++:

и список некоторых библиотек для других языков.


Это намного меньше о модели актера и намного больше о том, как трудно правильно написать что-то аналогичное OTP в C++. Кроме того, различные операционные системы обеспечивают радикально различную отладку и системный инструментарий, а виртуальная машина Erlang и несколько языковых конструкций поддерживают единый способ выяснить, что все эти процессы до которых было бы очень трудно сделать единым способом (или, возможно, сделать вообще) на нескольких платформах. (Важно помнить, что Erlang/OTP до нынешнего шума по поводу термина "модель актера", поэтому в некоторых случаях такого рода дискуссии сравнивают яблоки и птеродактили; великие идеи склонны к независимому изобретению.)

все это означает, что, хотя вы, безусловно, можете написать "модель актера" набор программ на другом языке (я знаю, я сделал это в течение длительного времени на Python, C и Guile, не осознавая этого, прежде чем я столкнулся с Erlang, включая форму мониторов и ссылок, и прежде чем я когда-либо слышал термин "модель актера"), понимая, как процессы вашего кода на самом деле порождают и что происходит из них крайне сложно. Erlang применяет правила, которые ОС просто не может без капитального ремонта ядра - капитального ремонта ядра, который, вероятно, не будет полезным в целом. Эти правила проявляются как общие ограничения для программиста (которые всегда можно обойти, если вам действительно нужно), так и основные обещания, которые система гарантирует программисту (что может быть намеренно сломан, если вам действительно нужно также).

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

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

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

Так что это не совсем о необходимости Erlang, язык, его о времени выполнения и OTP уже существуют, выражаются довольно чистым способом, и реализация чего-либо близкого к нему на другом языке чрезвычайно трудно. OTP - это просто трудный акт. В том же духе, мы действительно не нужно в C++, либо, мы могли бы просто придерживаться чтобы сырой двоичный вход, Brainfuck и рассмотреть ассемблер наш язык высокого уровня. Нам также не нужны поезда или корабли, так как мы все знаем, как ходить и плавать.

все сказанное, байт-код виртуальной машины хорошо документирован, и появился ряд альтернативных языков, которые компилируются в него или работают со средой выполнения Erlang. Если мы разбьем вопрос на часть языка / синтаксиса ("должен ли я понимать руны Луны, чтобы делать параллелизм?") и часть платформы ("ОТП самый зрелый путь сделать параллелизм, и поможет ли он мне обойти самые хитрые, наиболее распространенные ловушки, которые можно найти в параллельной распределенной среде?") тогда ответ ("нет", "да").


Касабланке еще один новый ребенок на блоке модели актера. Типичный асинхронный accept выглядит так:

PID replyTo;
NameQuery request;
accept_request().then([=](std::tuple<NameQuery,PID> request)
{
   if (std::get<0>(request) == FirstName)
       std::get<1>(request).send("Niklas");
   else
       std::get<1>(request).send("Gustafsson");
}

(лично я считаю, что CAF делает лучше прячет шаблону за приятный интерфейс.)