0,0
рейтинг
23 ноября 2014 в 16:23

Разработка → Web-разработка на Python глазами PHP-программиста

Введение



В статье хотелось бы поднять вопросы отличия использования Python для web-разработки по сравнению с оной на PHP. Надеюсь, статья не приведет к холиварам, так как она вовсе не о том, какой язык лучше или хуже, а исключительно о технических особенностях Python.


Немного о самих языках


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

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

Исходя из вышеописанных особенностей вытекают и различия в обработке ошибок в web-приложениях. В PHP существует целый зоопарк типов ошибок (errors, exceptions), далеко не каждую из которых можно перехватить, хотя это (невозможность перехвата) и не имеет большого значения, так как приложение живет ровно столько, сколько обрабатывается один запрос. Неперехваченная ошибка просто приводит к досрочному выходу из обработчика, и удалению приложения из памяти. Новый запрос будет обрабатываться новым «чистым» приложением. В Python же приложение постоянно находится в памяти, обрабатывая множество запросов без «перезагрузки». Таким образом поддерживать правильное предсказуемое состояние приложения крайне важно. Все ошибки используют стандартный механизм исключений и могут быть перехвачены (разве что за исключением SyntaxError). Неперехваченная ошибка приведет к завершению приложения, которое понадобится перезапускать извне.

Существует множество способов «приготовить» PHP и Python для веба. Далее я остановлюсь на двух наиболее мне знакомых (и кажется наиболее популярных) — PHP + FastCGI (php-fpm) и Python + WSGI (uWSGI). Конечно же, перед обоими этими связками предполагается наличие фронтенд-сервера (например, Nginx).

Поддержка многопоточности Python


Запуск сервера приложений (например, uWSGI) приводит к загрузке интерпретатора Python в память, а затем загрузке самого web-приложения. Обычно bootstrap-модуль приложения импортирует необходимые ему модули, производит вызовы инициализации и в итоге экспортирует подготовленный callable объект, соответствующий спецификации WSGI. Как известно, при первом импорте Python-модулей, код внутри них исполняется, в том числе создаются и инициализируются значениями переменные. Между двумя последовательными HTTP-запросами состояние интерпретатора не сбрасывается, следовательно сохраняются значения всех переменных уровня модуля.

Напишем простейшее WSGI-приложение, которое наглядно продемонстрирует вышеописанное на примере:

n = 0

def app(env, start_response):
    global n
    n += 1
    response = "%.6d" % n

    start_response('200 OK', [('Content-Type', 'text/plain')])
    return [bytes(response, 'utf-8')]

Здесь n является переменной модуля и она будет создана со значением 0 при загрузке приложения в память следующей командой:

uwsgi --socket 127.0.0.1:8080 --protocol http --single-interpreter --processes 1 -w app:app

Само приложение просто выводит на страницу значение переменной n. Для заядлых PHP программистов оно выглядит бессмысленным, так как «должно» каждый раз выводить на страницу строку «000001».

Проведем тест:

ab -n 500 -c 50 http://127.0.0.1:8080/
curl http://127.0.0.1:8080 

В результате мы получим строку «000501», что подтверждает наше утверждение, о том, что приложение находится загруженным в память uwsgi и сохраняет свое состояние между запросами.

Если запустить uWSGI с параметром --processes 2 и провести тот же тест, то несколько последовательных вызовов curl покажут, что мы имеем уже 2 различные возрастающие последовательности. Так как ab посылает 500 запросов, примерно половина из них приходится на один процесс uWSGI, а остальные — на второй. Ожидаемые значения, возращаемые curl будут примерно «000220» и «000280». Интерпретатор Python, судя по всему, один на процесс, и мы имеем 2 независимых окружения и реальную параллельную обработку запросов (в случае многоядерного процессора).

Python поддерживает потоки, как часть языка. Классическая реализация (CPython) использует нативные потоки OS, но есть GIL — в один момент времени выполняется только один поток. При этом все равно возможны проблемы race condition, так как даже n += 1 не является атомарной операцией.

Дизассемблируем python код
«Дизассемблируем» наше WSGI-приложение:
import dis

n = 0

def app(env, start_response):
    global n
    n += 1

    return [bytes(str(n), 'utf-8')]

if '__main__' == __name__:
    print(dis.dis(app))

  8           0 LOAD_GLOBAL              0 (n)
              3 LOAD_CONST               1 (1)
              6 INPLACE_ADD
              7 STORE_GLOBAL             0 (n)

 10          10 LOAD_GLOBAL              1 (bytes)
             13 LOAD_GLOBAL              2 (str)
             16 LOAD_GLOBAL              0 (n)
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 LOAD_CONST               2 ('utf-8')
             25 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             28 BUILD_LIST               1
             31 RETURN_VALUE

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

Увеличение количества потоков при отсуствии ожидания IO в коде обработчиков HTTP-запросов не приводит к ускорению обработки (а скорее даже ее замедляет, так как потоки могут «толкаться», переключая контексты). Реальной параллельности потоки в следствие ограничеия GIL не создают, хоть и являются не green thread'ами, а настоящими потоками OS.

Проведем еще один тест. Запустим uwsgi с 1 процессом, но 10 потоками-обработчиками в нем:

uwsgi --socket 127.0.0.1:8080 --protocol http --single-interpreter --processes 1 --threads 10 -w app:app

и выполним с помощью ab 5000 запросов к приложению.

ab -n 5000 -c 50 http://127.0.0.1:8080/

Последующие запросы curl 127.0.0.1:8080 покажут, что мы имеем только одну возрастающую последовательность, значение которой <= 5000 (меньше 5000 оно может быть в случае race condition на инкременте).

Влияние языка на архитектуру приложения


Каждый HTTP-запрос обрабатывается в отдельном потоке (справедливо и для процессов, так как процесс имеет минимум 1 поток). При этом каждый поток за время своей жизни (которое в идеальных условиях совпадает со временем жизни всего uwsgi приложения) обрабатывает множество HTTP-запросов, сохраняя свое состояние (т.е. значения переменных уровня модулей) от запроса к запросу. В этом заключается чуть ли не основное отличие от модели обработки HTTP-запросов в PHP, где каждый запрос приходит новое только что проинициализированное окружение и загрузку приложения необходимо выполнять каждый раз заново.

Типичным подходом в крупных web-приложениях на PHP является использование Dependency Injection Container для управления инициализацией и доступом к уровню сервисов приложения. Наглядным примером является Pimple. На каждый HTTP-запрос первым делом выполняется код инициализации, регистрирующий все доступные сервисы в контейнере. Далее по мере необходимости осуществляется доступ к объектом сервисов (lazy) в контроллерах. Каждый сервис может зависеть от других сервисов, зависимости разрешаются опять же через контейнер в коде инициализации сервиса-агрегата.

// Определяем сервисы
$container['session_storage'] = function ($c) {
    return new SessionStorage('SESSION_ID');
};

$container['session'] = function ($c) {
    return new Session($c['session_storage']);
};

// Используем сервисы
class MyController {
    public function __construct() {
        // get the session object
        $this->session = $container['session'];  // "тестриуемость" страдает, но не суть
    }
}

Благодаря контейнеру, можно обеспечить единовременное создание объектов и возвращение уже готовых объектов на каждое последующее обращение к сервису (если необходимо). Но эта магия работает только в рамках одного HTTP-запроса, поэтому сервисы можно без проблем инициализировать специфичными для запроса значениями. Такие значения зачастую — это текущий авторизованный пользователь, сессия текущего пользователя, собственно HTTP-запрос и пр. В конце запроса сервисы все равно будут разрушены, а в начале обработки следующего запроса — созданы и проинициализированы новые. К тому же можно практически не беспокоиться об утечках памяти, если обработка одного HTTP-запроса умещается в отведенные для скрипта лимиты, так как создание сервисов происходит по требованию (lazy) и на один запрос каждый необходимый сервис скорее всего будет создан только в единственном экземпляре.

Теперь, принимая во внимание вышеописанную поточную модель Python, можно заметить, что использование аналогичного подхода в Python web-приложении не возможно без дополнительных усилий. Если контейнер будет являться переменной уровня модуля (что выглядит вполне логично), все сервисы, которые он содержит, не могут быть проинициализированы специфичными для текущего запроса значениями, так как сервис будет являться разделяемым ресурсом между несколькими потоками, выполяющими обработку нескольких HTTP-запросов псевдо-параллельно. На первый взгляд существует два способа справиться с этой проблемой — сделать объекты сервисов независимыми от текущего HTTP-запроса (зависимыми остануться вызовы методов сервисов, а стековые переменные, используемые в методах, не являются разделяемыми ресурсами) или же сделать контейнер — ресурсом потока, а не процесса (тогда каждый поток будет общаться только со своим независимым набором сервисов, а в один момент времени один поток может обрабатывать только один HTTP-запрос).

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

Второй подход можно реализовать с использованием threading.local. Так, например, поступает flask. Чтобы проиллюстрировать подход, можно реализовать локальное для потока хранилище некоторых событий:

class EventsStore:
    def __init__(self):
        self._store = threading.local()

    def add(self, event):
        self.get_all().append(event)

    def clear(self):
        if self.has():
            del self._store.events

    def get_all(self):
        if not self.has():
            self._store.events = []

        return self._store.events

    def has(self):
        return hasattr(self._store, 'events')

    def pop_event(self):
        return self._store.events.pop() if self.has() and self._store.events else None

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

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

Заключение


Языки разные — подходы разные. Python имеет свои особенности и мне пока не до конца понятно, как их использовать правильно. Python предоставляет возможность иметь «состояние» приложения и многопоточную обработку запросов, но за это приходится платить потенциально возможными race condition (в свете того, что даже n += 1 не является атомарной операцией) и возможно более сложной архитектурой приложения. Хотелось бы услышать, как бородатые Python-разработчики строят свои web-приложения.
Иван Величко @Ostrovski
карма
56,0
рейтинг 0,0
Software Engineer
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (95)

  • –3
    Если вы воспользуетесь phpDaemon то у вас тоже будет «возможность иметь «состояние» приложения и многопоточную обработку запросов» на PHP.
    • +5
      ReactPHP лучше. Было несколько любопытных проектов запускать Symfony2 и Silex из под ReactPHP, получая при этом довольно неплохой прирост производительности за счет того что контейнер зависимостей всегда весит в памяти и не переинициализируется, так же как и пул соединений с базой и некоторые другие штуки. Но к сожалению я не знаю ни огдного проекта кто живет в продакшене на подобных штуках и при этом реализуют что-то большее чем обработчик очереди.
      • +1
        И как эти проекты себя вели? Ничего не сломалось при переходе? Просто помнится Фабьен как-то говорил на конференции про проблему перерождения всего приложения на каждом реквесте и о том, что хочет ее решить. Но ReactPHP вроде как не упоминал.
      • 0
        Есть примеры кода, для таких связок?
        • 0
          marcjschmidt.de/blog/2014/02/08/php-high-performance.html + www.reddit.com/r/PHP/comments/1yzcvi/bring_high_performance_into_your_php_app_with/
          github.com/reactphp/espresso — Silex + ReactPHP
          github.com/Blackshawk/SymfonyReactorBundle — дальше простого эксперемента дело не ушло.

          Так же видел куски реализаций пулов для доктрины, но там еще pthreads были примешаны и в опенсурсе подобного нет.

          Так же подобные штуки не для всех. То есть, сразу возникает необходимость разруливать конкурентные запросы, или делать надстройки для контейнера зависимостей (не всем сервисам можно жить вечно, некоторые должны умирать что бы ничего не поломалось). Словом… проблем явно хватает и что бы сделать возможным реализовать свое SAPI на уровень выше (на уровне httpkernel а не на уровне php) нужно оочень много работать.
    • +3
      phpDaemon умер так и не родившись, имхо. Нынче моден React ( reactphp.org/ ) у которого и исходники получше и работать он умеет не только с libevent ;)

      З.Ы. пока читал статью Festor успел первым упомянуть реакт.
      • –1
        На сколько вижу по сорсам, только с либевент и умеет (не считая различных реализаций). stream_select не даёт асинхронности.
        Как и говорил в своей статье на тему реакта, либевент, на мой взгляд, костыль! особенно учитывая что в php5.5 есть генераторы.

        На данный момент склонен думать, что phthreads — наилучшее решение для многопоточности в PHP.
        • 0
          libevent/libev это event-loop а не многопоточность, там главная идея минимизировать потери по времени между обращениями к i/o. То есть цели распаралелить выполнение кода никто не ставит — только мультиплексирование I/O. Так же это дает преимущество — меньше локов так как в один момент времени доступ к общим ресурсам имеет только кто-то один.

          Есть так же довольно популярная идея по использованию нескольких event-looop-в внутри нескольких процессов/потоков воркеров (процессы надежнее так как при падении не потянут за собой все). Насколько я помню что-то подобное применяется в том же nginx. Но это уже порядочно так усложняет архитектуру приложения. Пара лишних локов и производительность сервера будет ниже чем у реализации на libev.
          • –1
            Видимо, не достаточно связанно изложил мысль…
            Я не говорил что libev это потоки, наоборот, намекнул, что это async выполнение (упомянув stream_select). А прыгнул я к потокам, т.к. большинство ресурсов в PHP являются блокирующими, и с async тут ловить нечего, ну или использовать, что то типа DNode.
            • 0
              Поэтому и рекомендуется использовать неблокирующие стримы и разруливать все через stream_select внутри. А что бы вручную не разруливать и нужен event-loop и колбэки. А там уже и промисы что бы сума не сойти.

              Ну и опять же — можно это все комбинировать. При большом уровне конкуренси потоки будут сильно хуже event-loop.
              • –1
                Ну ресурсы этож не только стримы, но еще и коннекты к базам, кои в большинстве своём блокирующие.

                Комбинировать можно, а нужно? Если говорить про реквест-респонсе, то проще обрабатывать каждый запрос в отдельном потоке и всё. Иначе выйдет мешанина из асинка и потоков…
                • 0
                  Если говорить про реквест-респонсе, то проще обрабатывать каждый запрос в отдельном потоке и всё. Иначе выйдет мешанина из асинка и потоков…

                  При большом уровне конкуренси потоки будут сильно хуже event-loop.


                  Что по поводу базы данных — обычно так и делают, живет себе отдельно пул потоков, по коннекшену на каждый и разруливается в главном треде. Это в случае если у нас там PDO и штуки типа Doctrine. Скажем с mysqli можно юзать неблокирующие вызовы (да, в php можно работать с базой «асинхронно» из коробки)
        • 0
          Судя по моим тестам — он (phthreads) творит какую-то магию, но ни разу не делает тредов ибо обычный sleep в одном из тредов накрывает все «потоки», включая основной.
          • –1
            Каюсь, лично не использовал. Но не верю, есть пруф?
            • 0
              Win7, php 5.5 x64 TS

              Сырцы:
              Скрытый текст
              <?php
              class My extends Thread
              {
                  public $pid = 0;
              
                  public function run()
                  {
                      echo 'Thread start #' . $this->pid . "\n";
                      sleep(1);
                      echo 'Thread end #' . $this->pid . "\n";
                  }
              }
              
              $time = time();
              
              for ($i=0; $i<4; $i++) {
                  $my = new My();
                  $my->pid = $i;
                  $my->start();
              }
              
              echo 'Time ' . (time() - $time) . 's' . "\n";
              


              Результат:
              Скрытый текст
              Thread start #0
              Thread end #0
              Thread start #1
              Thread end #1
              Thread start #2
              Thread end #2
              Thread start #3
              Time 3s
              Thread end #3
              • 0
                Fesor авторитетно утверждает что sleep() стопит весь процесс, потому, данный пример некорректен для проверки.
                • 0
                  Ничего не авторитетно, это лишь предположение. Так как по итогу слип вызывается из главного потока и стопит все, но я не уверен.
                  • +1
                    Извините, ну тогда стоило бы отметить, что это предположение.
                • 0
                  Ну вот без слипа:
                  Скрытый текст
                  <?php
                  class My extends Thread
                  {
                      public $pid = 0;
                  
                      public function run()
                      {
                          echo 'Thread start #' . $this->pid . "\n";
                          ob_start();
                          for($i=0; $i<999999; $i++) {
                              echo 'num: ' . $i;
                          }
                          ob_end_clean();
                          echo 'Thread end #' . $this->pid . "\n";
                      }
                  }
                  
                  $time = microtime(true);
                  
                  for ($i=0; $i<5; $i++) {
                      $my = new My();
                      $my->pid = $i;
                      $my->start();
                  }
                  
                  echo 'Time ' . (microtime(true) - $time) . 'ms' . "\n";
                  



                  В результате такая же картина:
                  Thread start #0
                  Thread end #0
                  Thread start #1
                  Thread end #1
                  Thread start #2
                  Thread end #2
                  Thread start #3
                  Thread end #3
                  Thread start #4
                  Time 1.370078086853ms
                  Thread end #4
                  • 0
                    Буферизацию можно убрать. Я подумал что пых проигнорирует цикл, если внутри нет никаких операций, ан нет, сейчас проверил — он не оптимизирует байткод. Так что если увеличить количество итераций цикла в треде и убрать эхо с буфером — будет тоже самое.
                  • 0
                    О, я разобрался. Походу Thread позволяет создавать только один единственный дополнительный поток, вот и весь секрет.
                    Скрытый текст
                    <?php
                    class Child extends Thread
                    {
                        public function run()
                        {
                            $time = microtime(true);
                            for($i=0; $i<9999999; $i++) {}
                            echo 'Thread time ' . (microtime(true) - $time) . 'ms' . "\n";
                        }
                    }
                    
                    $time = microtime(true);
                    
                    $my = new Child();
                    $my->start();
                    
                    for ($i=0; $i<9999999; $i++) {}
                    echo 'Main time ' . (microtime(true) - $time) . 'ms' . "\n";
                    
                    Main time 0.70203995704651ms
                    Thread time 0.71104001998901ms
                    • 0
                      Не должно быть так по идее… надо разбираться
          • 0
            sleep стопит весь процесс. Во всяком случае без кода проблемного сложно придумать что-то другое.
    • +1
      еще лучше reactphp.org/ — nodejs на php :)
      • +1
        упс, каюсь — не рефрешнул
    • 0
      ещё есть github.com/amphp
      • 0
        Любопытно почему в README у них написано php5.5+ а в composer.json — 5.4.0+

        В целом реализация крайне любопытная но пока молодая и сырая.
        • 0
          На счёт реализации: Наверное стоит упомянуть, что React почти полная адаптация аналогичного решения в node.js.
          • 0
            Под «аналогичным решением» вы подразумеваете обычный event-loop?
            • 0
              На работе соорудил чатик в локалке на Racthet (ну потому что половина на аське, половина в почте, половина в скайпе и т.д., а тут просто говоришь «в чатик зайди» и всё), показал исходники коллеге, а он будучи знаком с нодой — похвастался, мол архитектура, почти все классы и идеи повторяют ноду.

              Яж не просто так столь уклончиво выразился «аналогичного решения», ибо реальным пруфом не обладаю, увы. Может кто-то из сообщества ткнёт носом в ссылку по ноде — был бы благодарен.
              • +1
                Node.js внутри использует libev, сама концепция event-loop довольно старая. Что до архитектуры чатика — есть различия так как php не js и некоторые вещи так же реализовать в прицнипе не выйдет. Но в целом… пишите вы чатик на php, js или erlang/go — будет примерно одинаково с точки зрения архитектуры. Тут как бы много чего не придумать. Это ж чатики.
                • 0
                  Помимо эвентлупа есть ещё много всякого, идея стримов (когда один можно вкладывать в другой, а тот в третий), те же промизы (о которых упоминали ниже), именование классов идентичное, методы и коллбеки всякие. Вон, про тот же phpDaemon не скажешь, что он слит с ноды.

                  З.Ы.Кусок композера из React — тоже использует libev:
                  {
                      "suggest": {
                          "ext-libevent": ">=0.1.0",
                          "ext-event": "~1.0",
                          "ext-libev": "*"
                      }
                  }
                  


                  • +1
                    Стримы используют все, кто знает чем они хороши. А хороши они тем что позволяют организовать единый интерфейс для взаимодействия с i/o. И не важно, сокеты внутри или файлы или еще что.

                    Про libev я о том и говорил что одна из реализация event-loop (там есть и просто на php и на старом добром libevent) в ReactPhp использует ту же библиотеку на которой построен node.js. Скажем в Python мире есть gevent который так же построен на libev, можете посмотреть их реализацию.

                    Но сама идея ReactPHP была в построении приложений схожих с node.js приложениями, так что я не спорю что многие идеи черпались у него. Хотя я не согласен что эти идеи принадлежат именно node.js.
            • 0
              Они даже промисы реализовали под php. Так что с ноды передрано чуть менее чем полностью.
              • 0
                Ну промисы слизали из Haskell справедливости ради, а может даже не у них. Эта идея основана на монадах.
  • 0
    Это свойство Python или uwsgi? Который с недавних пор умеет и php uwsgi-docs.readthedocs.org/en/latest/PHP.html
    • +2
      По ссылке же написано «The downside of this approach is the latency caused by the spawn of a new PHP interpreter at each request», то есть как обычно на каждый запрос запускается интерпретатор
      • 0
        Вопрос был о другом
    • 0
      Свойство сохранять состояние между запросами? Сдается мне, что если использовать Python, как CGI-скрипт, то он ничем не будет отличаться от PHP или даже C. С другой стороны пары uWSGI + Python и php-fpm + PHP являются весьма похожими конфигурациями (т.н. сервера приложений). Но первая сохраняет свое состояние, а вторая — нет. Как будет себя вести PHP с uWSGI я не проверял, но по логике вещей, он должен вести себя также, как и с php-fpm, т.е. сбрасывать интерпретатор от запроса к запросу.
      • +1
        Но ведь к ЯП это никакого отношения не имеет? Это уже middleware. Там выше уже написали, что и под PHP есть веб-сервер, позволяющий хранить состояния.
        • +1
          К спецификации ЯП это наверное отношения не имеет. А вот к конкретной реализации ЯП это имеет большое отношение. У реализации PHP, например, есть свои особенности (см. линк) и в итоге для PHP оказывается неестественно сохранять свое состояние от запроса к запросу или же использоваться как язык для разработки долгоживущих демонов.
          • +2
            Собственно по поводу сохранения состояния между запросами. Так реализован сам интерпретатор что не может. Запуская код (для php по сути «скрипт») — он инициализирует окружение (собственно его начальное состояние с загруженными extensions, конфигурацией из php.ini, а так же со всеми переменными $_SERVER, $_REQUEST и прочим) и потом начинает выполнение самого кода. Так вот, если выполнить инициализацию окружения повторно (даже не убивая сам процесс) — почистится все наглухо (так и работает fpm — обработал запрос, почистил, обработал запрос, ...). Если же этого не сделать, а просто запустить другой (или этот же) — отвалится часть окружения, конфигурация из php.ini, extensions, в том числе и необходимые переменные для работы «скрипта» (тот же $_REQUEST).
            Тут дело не в языке, дело в реализации интерпретатора
            • +4
              Напимер те же extension имеют вхождения .MINIT и .MSHUTDOWN (в названиях могу путаться), но суть сохраняется. MINIT вызывается при инициализации, MSHUTDOWN при выгрузке модуля. Обычно в MINIT делают всякие malloc и обнудения переменных, в MSHUTDOWN free. Ну это так, по простому. Инициализация с сброс окружения вызывают соответственно MINIT и MSHUTDOWN каждого расширения. Ну если окружение не сбросить — память потечет во всех расширениях, если не сделать инициализацию — будут segfault-ы. Вот он и порочный круг php — инициализация, выполнения, сброс. Это зашито в архитектуре.
              Аналогично и с php.ini и с прочим.
              • 0
                Вы в исходниках сами разбирались или какую-то литературу читали на данную тему? Хочется в вопросе разобраться поглубже. До этой статьи не задумывался о таких вещах даже.
                • +1
                  Качайте сорсы, в них многое понятно, если есть опыт работы с C. Книг обосо хороших я не нашел. Только вот эта, но на мой взгляд много воды.
                • +1
                  Начал с ковыряния в расширениях php, потом решил сделать собственный SAPI для php. Собственно горел идеей сделать php без сброса окружения, и как оказалось сделать это крайне сложно, ибо придется переделывать архитектуру самово интерпитатора. Забил на это занятие.

                  А информация, да, из сырцов. Там все довольно просто, кроме того, что все раскидано по файлам хаотично.
  • +4
    В статье не указано, что не все web-приложения на Python сводятся к WSGI. Контрпример — приложения, основанные на фреймворке Tornado. Там становится неверным утверждение «каждый HTTP-запрос обрабатывается в отдельном потоке».

    Но наиболее распространенный сценарий описан верно.
    • 0
      Да, я сознательно остановился только на WSGI приложениях, чтобы сравнить их со схожей моделью FastCGI PHP.
  • +4
    GIL — в один момент времени выполняется только один поток
    Не совсем так, лучше сказать, что в один момент работает только один питон код. При этом IO или вызовы расширений (написанных например на С) будут работать параллельно. Например можно загрузить все ядра одним процессом где в разных потоках запустить распаковку/запаковку данных.
    • 0
      Спасибо за поправку. Нужно мне еще покурить этот ваш GIL, чтобы разобраться в нем как следует.
  • +2
    >Если же приложение достаточно большое и обладает внушительным количеством сервисов, то после обработки определенного числа HTTP-запросов, подавляющее большинство сервисов окажется проинициализировнными и находящимися в памяти процесса. Выглядит так, что это может оказаться серьезной проблемой.

    Не секрет, что в большинстве случае инициализированные сервисы используются постоянно, а не только какими-то редкими вызовами. И никто не запрещает использовать для таких случаев threadlocal инстансы. Вот это и будет золотой серединой — бОльшая сервисов инициализируется сразу и являются долгоживущими, а какие-то создаются как threadlocal.
    • 0
      Отлично! А есть на примете примеры приложений, на которых можно было бы увидеть использование такого подхода? К сожалению те web-приложения, которые я успел просмотреть на github не тянут на «большие» и грешат, грубо говоря, бизнес-логикой в контроллерах. Уровнем сревисов там зачастую и не пахнет.
      • 0
        Я такой подход я видел в одном крупном закрытом приложении, про публичные приложения не подскажу, так как все обычно ровно как вы описываете.
      • +1
        Теоретически вот это вполне тянет на большое с сервисным уровнем: code.edx.org/
      • 0
        Популярные микро и не очень фреймворки Flask, Bottle, Pyramid и т.д. используют этот подход, чем серьезно улучшают качество жизни python веб-программистов)
      • +1
        Недавно наваял WSGI микро-фреймворк на Python: marnadi.

        PS: так же как и вы, перешел в свое время с PHP на Python, о чем не жалею нисколько
  • –4
    n += 1 является именно атомарной операцией, по крайней мере для int и float. Не стоит писать неверные утверждения, это запутывает.

    Для пользовательских типов данных — да, чаще всего будет неатомарно.
    • 0
      Так, а как же тогда интерпретировать результаты дизассемблирования? Ни коим образом не спорю, просто действительно хочу узнать истину.
      • –6
        Смотрим на дизассемблер:
        In [1]: import dis
        
        In [2]: def f(a, b):
           ...:     a += b
           ...:     
        
        In [3]: dis.dis(f)
          2           0 LOAD_FAST                0 (a)
                      3 LOAD_FAST                1 (b)
                      6 INPLACE_ADD
                      7 STORE_FAST               0 (a)
                     10 LOAD_CONST               0 (None)
                     13 RETURN_VALUE
        

        Нужная нам инструкция — это INPLACE_ADD. Один opcode, значит переключения GIL не происходит.
        • +5
          Здесь INPLACE_ADD будет делать ровно то же самое, что и BINARY_ADD. Опкодов два: INPLACE_ADD и STORE_FAST. Уберите STORE_FAST и получите, что ничего не изменилось.

          Это не список, у которого определён __iadd__. Это неизменяемое целое.
          • 0
            Ну, то есть, не здесь, а у Ostrovski. Целое у него.
            • +2
              По этой же причине нельзя забывать про LOAD_GLOBAL (у вас его нету). Если значение n изменится после LOAD_GLOBAL, но перед STORE_GLOBAL, то результат будет некорректным. А это три промежутка, куда можно вклинится.
              • +2
                OK, вы правы.
  • +17
    Бородатые Python-разработчики строят свои приложения на фреймворках. Низкоуровневые аппликухи — это то, за что проект-манагер должен бить по рукам, потому что столь низкоуровневое программирование есть просто потеря времени. Нужна сложность и мощь? Django. Нужна высокая скорость, хайлоуд и прочее? Tornado. Нужно что-то простое, не городя огород? Flask, bottlepy и прочие.
    • +2
      Возможно я невнимательно смотрел, но что-то во flask я не заметил уровня, где можно было бы разместить пользовательские сервисы. Фреймворк для меня скорее выступает как точка входа в приложение, предоставляя такой базовый функционал, как роутинг и шаблонизация, например. И я стараюсь избегать влияния фреймворка на архитектуру приложения, если оно чуть сложнее, чем персональная страничка.
      • 0
        Канонический способ сделать там такое, это написать расширение. На примере как раз и рассматривается подключение к БД.
        И я стараюсь избегать влияния фреймворка на архитектуру приложения, если оно чуть сложнее, чем персональная страничка.
        А фреймворк разве не по определению жестко задает структуру приложения, во всяком случае на низших уровнях?
        • +2
          Именно по определению фреймворк скорее всего задает структуру приложения. С другой стороны фреймворки должны удовлетворять массовому спросу, позволяя ускорять разработку типовых решений. Я же придерживаюсь мнения, что если приложение чуть менее, чем стандартное, то лучше его сделать максимально обособленным от фреймворка, оставив за последним право быть точками входа в это самое приложение. Тогда имеешь куда больший контроль над системой и не приходится «бороться» с фреймворком.
          • +1
            Не знаю, имеет ли смысл углубляться в дебри для приложения чуть сложнее чем персональная страничка. Тем более, что Flask. Bottle, etc — не зря называются микрофреймворками. Они собственно только и делаю, что прячут «дебри», и «наворачивают» не так сильно всего, как полные фреймворки. Если переводить на пиэчпишные понятия, то они скорее коррелируют в этом плане со Silex. При желании довести их до уровня Symfony2 возможно написанием расширений. Хотя уровень Symfony2 «из коробки» в питоне тоже представлен, это например Django.
    • 0
      Да, а многопоточность решается gevent и multiprocessing.
  • 0
    (промахнулся веткой)
  • +1
    Все ошибки используют стандартный механизм исключений и могут быть перехвачены (разве что за исключением SyntaxError).
    Как это как это.

    In [2]: try:
       ...:   import test
       ...: except Exception as e:
       ...:   pass
       ...: 
    
    In [3]: e
    Out[3]: SyntaxError('invalid syntax', ('test.py', 1, 10, 'import def\n'))
    
    • 0
      Таким же образом можно перехватить SyntaxError в eval, например. А вот в текущем файле — врядли, так как интерпретатор парсит файл целиком, и, если ошибка синтаксиса внутри блока try, ее все равно не удасться перехватить, так как в общем случае не известно, как обрабатывать следующий за ней код.
      • +3
        Вы хотите странного. Нет никакого «следующего за синтаксической ошибкой кода», весь код который следует за ошибкой — не верен и не может быть исполнен при всем желании. Ошибка происходит именно на этапе импорта, там её и можно отловить.
        • +2
          Я как раз не хочу. Просто пояснил свою мысль из статьи по поводу перехвата SyntaxError. Согласен, что код, следующей за синтаксической ошибкой, в общем случае не возможно интерпретировать онднозначно.
        • +1
          Это для Python (и для большинства остальных языков, где есть нечто подобное). В некоторых языках такое поведение будет неожиданным. Правда я из таких знаю только VimL¹, а это никак не образец хорошего внутреннего устройства. Про PHP не знаю.

          ¹ Ошибки, использующиеся вместо SyntaxError, можно отловить прямо на месте, т.к. в VimL команда всегда заканчивается в конце строки, если только используемый метод получения строк не предоставляет их в виде одной \n‐разделённой C’шной строки (особенность реализации, которую мне придётся поддерживать).
  • –3
    В ведении исправьте пожалуйста «web-зазработки»
  • 0
    *miss*
  • +4
    Wow, вдохновляющая статья, как раз начал уход от php в сторону python. Посоветуете какую-нибудь литературу на данную тему?
    • 0
      Позвольте поинтересоваться, а в чём причина именно «ухода»?
      • 0
        Причина исключительно личного предпочтения. Меня php развратил. Есть функции и инструменты на все случаи жизни, которые я никак не могу запомнить, но легко могу научиться использовать. В итоге во мне умирает программист. (:
        • +2
          Тогда, думаю Вам стоит переходить на asm… =)
        • +1
          Есть функции и инструменты на все случаи жизни, которые я никак не могу запомнить, но легко могу научиться использовать.
          В python с этим еще круче.
    • +1
      Я начал с книжек по Django, но очень быстро перешел на официальную документацию, там всё вполне последовательно и понятно:

      docs.djangoproject.com/en/
    • 0
      Рекомендую «Two scoops of Django. Best practice for Django 1.6»
  • –1
    >за это приходится платить потенциально возможными race condition

    Есть глобальное мутабельное состояние — ставь лок. Python тут не уникален.
  • 0
    Хорошая статья, видно вы старались.
  • –5
    Нельзя так сравнивать две вещи и писать, что один язык умирающий, а второй универсальный. Помню в середине 90х читая Г. Буча «Объектно-ориентированный дизайн и программирование» сразу же уперся в ряд голословных утверждений о том что ООП лучший подход для всего в программировании.

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

    Сравнение должно происходить по заданным критериям. Для чего-то лучше одно, для других задач — другое.
    • +4
      Простите, Вы статью читали?)
      • –4
        Да, там «универсальный язык программирования» еще и с ошибкой написано. Универсальных языков программирования не бывает. Как не бывает универсального инструмента на все случаи жизни.
        • +4
          Когда говорят «универсальный язык программирования» обычно имеют ввиду язык, который
          1. Изначально предназначался для решения широкого спектра задач.
          2. Является Тьюринг‐полным.
          3. На котором при этом люди реально могут решить практически всё — от написания текстового редактора до 3D‐рендеринга. Разумеется, это «всё» они могли бы решить лучше (быстрее и/или с меньшими затратами ресурсов компьютера) на других языках… если бы умели на них писать, причём быстро.
          При этом из «всего» на самом деле исключены узкоспециализированные задачи: написание прошивок, скриптование ПО, не поддерживающего язык, …

          В общем, данный термин не следует воспринимать совсем уж буквально. Текст или речь, произведённые людьми вообще не стоит воспринимать буквально практически никогда — математические доказательства есть практически единственные тексты, автор которых предполагал или должен был предполагать их буквальную интерпретацию.
        • 0
          Вы точно не бот? Если нет, то все же прочитайте статью, она совсем о другом.
  • –6
    «имеют в виду» пишется в три слова. Остальное словоблудие. Насколько широкого спектра? В чем он измеряется? Решить можно что-то виртуально? Практически не все? Программирование относится к техническим наукам, и тут важна четкость как в математике. А то потом начинают запожные гвозди кувалдой заколачивать. Ради скрипта элементарного фреймворк устанавливают.
    • +3
      Как же все заморочено…
      Есть давно устоявшаяся терминология. Может так понятнее будет en.wikipedia.org/wiki/General-purpose_programming_language
    • +1
      Здесь не научная статья. Никому низачем не нужно чёткого определения термина «язык общего назначения». Совсем. Умные люди отлично понимают нечёткие определения, тем более, что в обычном толковом словаре большинство именно такие.

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