Stackless Python и Concurrence

    Перед тем, как перейти собственно к возможностям Stackless и Concurrence, рассмотрим самый простой способ написать сетевое приложение, обрабатывающее несколько одновременных соединений:

    socket()
    bind()
    listen()
    accept()
    fork() ->
        read()
        write()
        ...
        close()
    

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

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

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

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

    В приложениях, основанных на явном задании конечного автомата, главный цикл представляет собой опрос всех открытых соединений на предмет возникновения каких-либо событий — пришли данные, произошла ошибка или освободилось место в буфере отправки. По каждому из событий вызывается обработчик у конечного автомата. Для оптимизации этого опроса (а быстро опросить десятки тысяч соединений — непростая задача) в современных операционных системах предусмотрены различные очень эффективные, но не совместимые друг с другом интерфейсы (kqueue, epoll и другие). Чтобы писать переносимые сетевые приложения, были разработаны специальные библиотеки типа libevent, скрывающие от программиста детали реализации опроса соединений.

    Однако, если мы задаём конечный автомат в явном виде (каждое состояние отдельным участком программы), то структура приложения становится сложной и нечитаемой. Фактически, переход между состояниями в этом случае аналогичен использованию операторов goto — когда меняется состояние, нам нужно заново искать по всему исходнику, где находится обработчик следующего состояния. Вот пример структуры приложения, реализующего простой сетевой протокол:

    select()
        -> read_ready ->
            read(cmd)
            if state == "STATE1":
                if cmd == "CMD1":
                    state = "STATE2"
                else:
                    invalid_command()
            elif state == "STATE2":
                if cmd == "CMD2":
                    state = "STATE1"
                else:
                    invalid_command()

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

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

    Stackless

    Есть способ совместить производительность конечных автоматов и простоту первого решения. Как раз для этого нам и понадобится Stackless Python. Stackless Python — это усовершенствованная версия интерпретатора Python. Она позволяет программисту пользоваться преимуществами многопоточного программирования без снижения производительности на примитивах синхронизации и без проблем с «гонками» (race conditions). Если правильно использовать дешёвые и лёгкие микропотоки Stackless, они позволяют улучшить структуру программы, получить более читаемый код и увеличить производительность труда программиста. Посмотрим, как это работает.

    С точки зрения программиста, создание тасклета (микропотока в терминах Stackless) ничем не отличается от создания нового потока операционной системы: stackless.tasklet(your_func)(1, 2, 3). Мы запускаем выполнение функции your_func(1, 2, 3) в контексте нового тасклета. Выполнение этой функции продолжится до тех пор, пока тасклет явно не отдаст управление ядру (stackless.schedule()), либо заблокируется, ожидая отправки или получения какой-то информации. Например, тасклет хочет получить данные из сетевого сокета, а они ещё недоступны. В этот момент тасклет встаёт в очередь ожидания ввода-вывода, и управление передаётся следующему по очереди тасклету. Когда придут ожидаемые данные, первый тасклет получит управление и продолжит обработку данных.

    По сути, та же самая логика работала и в схеме с конечными автоматами (кооперативная многозадачность, необходимость использования диспетчера сокетов и отсутствие необходимости в примитивах синхронизации для доступа к общим структурам данных), главное отличие — в том, что задача описывается обычным линейным кодом на Python. Для примера, обращения к сетевым сервисам могут описываться так:

    val = memcached.get("some-object-123")
    if val is None:
        res = list(mysql.query("select val from tbl where id=%d", 123))
        if len(res):
           val = res[0]
           memcached.set("some-object-123", val)

    Каждая из сетевых операций (обращения к memcached, базе данных, выполнение HTTP-запросов, отправка писем по SMTP и т.д.) будет приостанавливать выполнение тасклета до получения её результа. Во время ожидания будут выполняться другие тасклеты.

    Тасклеты могут отправлять друг другу данные, пользуясь каналами (channels). Канал — это объект, имеющий два основных метода — send() и receive(). Если один тасклет отправляет в канал данные ch.send(some_object), то другой может эти данные получить: some_object = ch.receive(). Если на канале нет ожидающего данных тасклета, то отправляющий будет заблокирован до тех пор, пока данные не будут получены. И наборот, если в канале нет ожидающих данных, то принимающий тасклет заблокируется до их появления. Одним каналом может пользоваться множество тасклетов, каждый из которых может принимать или отправлять данные. Каналы — это основной метод синхронизации между тасклетами. К примеру, если вы хотите реализовать пул из ограниченного числа постоянных соединений с базой данных, то операция взятия соединения из пула может быть такой:

    def get():
        if len(self._pool):
            return self._pool.pop(0)
        else:
            return self._wait_channel.receive()

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

    def put(conn):
        if self._wait_channel.balance < 0:
            self._wait_channel.send(conn)
        else:
            self._pool.append(conn)

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

    Сам Stackless представляет собой систему переключения контекста тасклетов, планировщик, механизм каналов и сериализацию тасклетов, позволяющую сохранять их на диск, передавать по сети, а затем продолжать выполнение с того места, где он был прерван. Есть ещё пакет greenlets, который представляет собой урезанную версию Stackless. В нём реализованы только микропотоки (собственно greenlets), а вся остальная логика, включая планировщик, ложится на плечи программиста. Из-за этого Greenlets немного (процентов на 10-25) медленее, чем Stackless, зато они не требуют специальной версии интерпретатора.

    Concurrence

    Для написания реальных сетевых приложений нужна библиотека для работы с неблокирующими сокетами, которая будет включать в себя диспетчер сокетов, блокирующий тасклеты на сетевых операциях и продолжающий их исполнение при возникновании сетевых событий. Таких библиотек есть несколько: простая мелочь, Eventlet (только для Greenlets), gevent (только для Greenlets) и Concurrence (для Greenlets и Stackless). Именно про последнюю я и хочу рассказать.

    Concurrence основан на libevent, его главный цикл и система буферов соединений реализованы на C и дают отличную производительность сетевых операций. Кроме собственно диспетчера сокетов, Concurrence предоставляет возможность создания таймеров, использования функций типа sleep(s), в нём реализованы многие популярные протоколы (HTTP-клиенты, HTTP-серверы (WSGI), Memcached, MySQL — да-да, настоящая асинхронная клиентская библиотека MySQL, XMPP). Пример, приведённый выше (с обращениями к Memcached и MySQL) написан именно на Concurrence. Вот как сделать с его помощью минимальный веб-сервер:

    def hello_world(environ, start_response):
        start_response("200 OK", [])
        return ["<html>Hello, world!</html>"]
    
    def main():
        server = WSGIServer(hello_world)
        server.serve(('localhost', 8000))
    
    dispatch(main)

    Функция dispatch запускает главный цикл Concurrence и ставит в очередь самый первый тасклет, выполняющий функцию main. Далее запускается WSGIServer, который и будет принимать соединения. Под каждое соединение запускается отдельный тасклет, выполняющий функцию hello_world. Последняя может быть произвольной сложности и включать в себя любые асинхронные операции. Пока система будет ожидать их выполнения, будут продолжать приниматься новые соединения.

    Теперь ложка дёгтя. К сожалению, похоже, Concurrence заброшен и более не поддерживается. На письма автор не отвечает, в том числе и на багрепорты с патчами. Поэтому я взял на себя смелость опубликовать свою версию Concurrence с исправленными багами, которые нашёл, и с несколькими дописанными фичами, в частности, с поддержкой HTTP PUT для WebDAV, с реализованным SMTP-клиентом и поддержкой Thrift. Репозиторий лежит на github.

    Всех, кто планирует использовать Stackless, Concurrence или другие технологии асинхронного программирования на Python, приглашаю подписаться на список рассылки ru-python-async.

    Ссылки

    Stackless Python — www.stackless.com
    Истории успеха — www.stackless.com/wiki/Applications
    Concurrence — opensource.hyves.org/concurrence
    Моя версия Concurrence — github.com/JoyTeam/concurrence
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 58
    • 0
      Простите за упрямство, но чем все-таки Stackless и Concurrence принципиально лучше встроенных Thread, ThreadPool, Queue для синхронизации и т.п.?

      Я вижу, что например Stackless имеет очень приятные фичи — kill для потока и сериализацию потока. Да, это круто. Но все же, с точки зрения именно параллельного программирования — чем оно принципиально лучше?
      • 0
        Принципиально ничем, только производительность выше. Для примера посмотрите на функцию get(), которую я привёл в посте. В многопоточной программе, чтобы избежать race condition, нужно было бы завернуть тело функции в мьютекс. В Stackless оно не надо, ибо многозадачность кооперативная.
        • –2
          > Принципиально ничем, только производительность выше.
          А вы точно сами понимаете, что написали? Именно принципиально они и отличаются.
        • 0
          ну например тем, что изза GIL писать многопоточные программы в python вообще не стоит, исключительно асинхронные вещи и IPC.
          • 0
            Хорошо, согласен. Но корпоративная многозадачность подразумевает явную передачу управления другому потоку. Как в таком случае реализовать, например, параллельную закачку файлов? Ведь библиотечный urlopen не имеет внутри закладок для передачи управления. Будет ли это работать?
            • +2
              urlopen не содержит закладок, да. Но вы пользуйтесь не urlopen, а HTTP-клиентом из Concurrence:

              conn = HTTPConnection()
              сonn.connect(("hostname", 80))
              try:
                  req = conn.post("/path/name", data)
                  req.add_header("Content-type", "multipart/form-data")
                  req.add_header("Content-length", len(data))
                  res = conn.perform(req)
              finally:
                  conn.close()

              Всё будет отлично работать впараллель.
              • 0
                Но, к сожалению, оно не обладает тем огромным функционалом, который предоставляет стандартная библиотека.
                • 0
                  gevent умеет патчить стандарные функции для работы с сокетами, делаях их асинхронными — после этого стандарные либы автоматически начинают работать асинхронно тоже.
                  • +1
                    упс, сначала читать все, потом писать — все время забываю )
                • 0
                  там urlopen заменен на асинхронную версию, т.е. когда вы делаете urlopen — управление передается интерпретатору.
                  в stackless для этого все написано несколько версий socket библиотек (e.g. stacklesssocket)

                  например что-то вроде:

                  import stacklesssocket
                  stacklesssocket.install()

                  будет манкипатчить стандартный сокет.
                  • 0
                    Это уже гораздо интересней. Но в общем случае, как я понял, если будет какая-то библиотека, которая не будет иметь этих закладок, и которая не будет обработана разработчиками stackless-а, то многозадачности уже не получится, правильно?

                    И второй вопрос: а нету ли оберток над этим делом, симулирующих стандартные интерфейсы Thread и ThreadPool? Честно скажу, с GIL-ом было много проблем, и я не против попробовать эту библиотеку, но переписывать на нее весь код — будет мягко говоря накладно. Притом, что я не вижу никаких особых препятствий сделать такую обертку.
                    • 0
                      стандартные операции с питоновскими сокетами — станут асинхронными, написаные без их участия — нет.

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

                      ну и обычные методы для IPC — amqp. или можно глянуть zeromq, если хочется совсем скорости
                    • 0
                      т.е. манкипатчинг стандартного сокета возможен… Это хорошо… А как быть например с запросами к MySQL через MySQLdb? Они будут блокировать интерпретатор глобально или во время запроса к БД смогут параллельно выполняться другие тасклеты?
                      • 0
                        Протокол MySQL полностью реализован на чистом пайтоне в Concurrence как раз для того, чтобы сделать его неблокирующимся.
                  • 0
                    Плохо, не согласен. Смысл есть. Просто он не в том, чтобы загрузить все ядра и получить бОльшую производительность.
                    Впрочем, ты сам сказал: «исключительно асинхронные вещи»… В теперь объясни, для чего ещё использовать несколько потоков в программе, как не для асинхронных вещей?
                    • 0
                      я так понимаю это коммент мне =)
                      асинхронное программирование, лично для меня, дает некоторые плюсы — отсутствие синхронизации. это очень здорово упрощает программирование, т.к. я здорово гробил силы на многопоточно-специфические вещи.
                      для чего стоит делать много потоков — именно загрузить много ядер, ведь если одно ядро справляется, а все остальное ввод-вывод, то смысл блокироваться на этих событиях? я понимаю, что проще использовать много потоков, что бы сделать то, что можно сделать асинхронно, но это только если ты привык писать многопоточные вещи.
                      • 0
                        Твой ответ запутал меня…
                        У меня достаточно опыта написания многопоточных программ (питон и Си) и всегда я использовал многопоточность для выполнения неких асинхронных действий. В общем-то ловил двух зайцев: хорошая, красивая архитектура приложения и отсутствие блокировки на IO.
                        И GIL мне совершенно не мешал. Если перегруз, то делаем распределитель и раскидываем действия на два-три системных потока и не паримся, так как затраты на их использования невелики (пока их два-три, конечно). Есть там, правда, пара подводных камней, но всё обходится и решается.
                  • –1
                    Может тем, что это не треды? Прочтите внимательнее, прочтите другие статьи, если эта не ясна.
                    И главное — тасклеты это coroutines, сопрограммы, но никак не треды/потоки!
                    • –2
                      Я не спрашивал, чем оно отличается. Я спрашивал, чем оно лучше. Это совершенно разные вопросы: мне нет смысла менять одну технологию на другую просто потому, что она другая (более продвинутая, более новая или еще какая-то более клевая), но при этом не дает ощутимой выгоды.

                      Поэтому прежде чем что-то делать, я хочу понять, а надо ли мне вообще это делать.
                    • 0
                      > очень приятные фичи — kill для потока
                      Когда я вижу такое, мне хочется выть… Убивать поток/процесс до его нормального завершения нельзя никогда. То есть вообще.
                      Нужно заставить поток завершиться самому. Корректно и без лишних воплей. Если у вас это не получается, значит вы где-то напортачили с архитектурой всего своего многопоточного хозяйства и делаете что-то не то.

                      Единственное исключение, это kill -9, но это немного из другой оперы.
                      • 0
                        > Когда я вижу такое, мне хочется выть… Убивать поток/процесс до его нормального завершения нельзя никогда. То есть вообще.

                        Не стоит быть настолько категоричным.

                        В потоке может выполняться совершенно различный код, в том числе тот, который вы сам не писали: это может быть API-функция, метод из сторонней библиотеки, реализация которого вас не интересует и т.п. А вам просто нужно, чтобы этот код выполнялся не более чем конечное время (возможно заранее заданное), в таком случае в «убийстве» потока нет ничего предрассудительного.
                        • 0
                          Ещё немного порекламирую Concurrence:

                          try:
                              with Timeout.push(10):
                                  do_something_heavy()
                          except TimeoutError:
                             do_some_recovery()

                          А API-функцию в асинхронной программе вы не прервёте. Пока она выполняется, другой код всё равно управление не получит.
                          • 0
                            Нет, возмущаться про убийство нитей можно долго… Но это красивый код. :-)
                            • 0
                              > А API-функцию в асинхронной программе вы не прервёте. Пока она выполняется, другой код всё равно управление не получит.

                              Я не знаю как в стеклесс, но в обычном питоне WinAPI функция CopyFile у меня прервалась при убийстве нити методом _Thread__stop().
                              • 0
                                В Stackless нет. Прерваться может только на моменте, когда тасклет добровольно отдаёт управление планировщику. Точнее, есть ещё режим вытесняющей многозадачности, когда можно по счётчику инструкций отбирать управление у тасклета, но к внешним по отношению к Python функциям это не относится.
                            • 0
                              Хм… Ну может быть. С таким не сталкивался.
                              Вообще, это всё равно плохо, так как провоцирует писать плохой код. Я вот, когда был молодым да зелёным, очень матерился на то, что нельзя просто убить нить. Qt'шную можно, а обычную нельзя! Это было очень неприятно и просто бесило. Потом, напарившись с неделю с одним странным глюком, я понял, что прибивать qt'шную нить нельзя в принципе. Никогда и ни за что. Лучше ей флажком со всей дури дать и подождать, пока она сама сдохнет.
                              • 0
                                Есть разные системы с разными подходами. Например, в OS Inferno неблокирующего I/O нет в принципе, любой read/write блокирующий. И единственный способ реализовать те же таймауты на read/write — убить нить. Так задумано архитектурно, и никогда из-за этого никаких «странных глюков» не возникает.
                                • 0
                                  Проблема с убийством нитей только в том, что в этот момент они могут находиться в весьма пикантных состояниях, например внутри malloc. Убил — нарушил работу аллокатора для всего процесса. Я не знаю, как вопрос решен в Inferno, но либо должны существовать какие-то меры против этого, либо безопасно убивать нити не получится.
                                  • +1
                                    В Inferno используется VM, и на Limbo нет прямой работы с памятью. Поэтому все malloc-и делаются VM, равно как и kill нити делается тоже VM. Так что у VM есть всё необходимое для того, чтобы убивать нить только когда это безопасно. Но программист ничего этого не видит, он просто может в любой момент безопасно убить любую нить.
                        • 0
                          и ни слова про epoll?
                          • +3
                            libevent, на котором построен Concurrence, умеет epoll.
                          • 0
                            Мне gevent больше импонирует, проще и быстрее.
                            • 0
                              Его хотели портировать под Stackess в рамках Google Summer of Code, но не портировали. Почему — не знаю, кстати. То ли заявку не приняли, то ли не сделал никто.
                              • 0
                                Там один разработчик :) Денис Биленко
                                Сейчас разбирается вопрос об инвестициях, но не знаю соглашается он или нет.
                            • 0
                              Понимаю, что вопрос скорей всего в пустоту (вряд ли этот пост будет просматривать человек который является специалистом и в java и в python), но всё же: за счёт чего stackless python может выиграть в производительности у sun/oracle JVM ну и я зыка JAVA?
                              Событийная работа с сокетами? есть NIO + библиотеки поверх них типа netty/MINA
                              Тасклеты и однопоточный планировщик, который который переключает тасклеты при блокировки? Ну так и в JVM — зеленые потоки и планировщик. нить заблокирована — управление передано другой нити. Так что на однопроцессорной однаядернйо машине — выполняется только один поток.
                              на многоядерной — можно привязать к определённому ядру. Чтобы ибежать кеша потока на общих данных использовать volitile.
                              Где выигрыш?
                              • +2
                                Выигрыш чего перед чем? Пост про Python, в общем-то. Если Java умеет поставить зелёный поток в слип на ожидании ввода-вывода, а когда надо разбудить, то это же здорово. Это как раз и есть та же технология, про которую я писал.
                                • 0
                                  Тут речь не о выигрыше в производительности, а в простоте разработки. Нету потоков — нету факапов с race conditions. Плюс сам по себе пайтон раз в десять-двадцать по скорости разработки фору джаве даст. На том и выезжают.
                                  • +1
                                    Я бы уточнил, что производительность все таки вырастает, засчет того, что поток исполнения не блокируется на ожидание IO. Другое дело, что coroutines не прерогатива python, и впервые были вообще не в python реализованы.

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

                                    Ну и потом — производителность java это производительность ее VM. Для python параллельно разрабатываются два jit, так что недолго ему осталось ходить в тормозах.
                                    • +2
                                      Что не нравится всегда можно в pyrex, cython :) или native C :D
                                      • 0
                                        А какой второй кстати? Кроме pypy?
                                        • 0
                                          unladen swallow
                                          • 0
                                            Так он же вроде сдулся? В смысле его допилили, он не дал такого буста как ожидалось, гугловцы написали что это потому что питон сам по себе такой медленный и ничего не поделать, и теперь его просто вольют в питон и все — никакой революции. Разве нет?
                                            • 0
                                              Нет, не сдулся. Js ничем не лучше, а буста ему те же гугловцы вдули от и до.
                                              • +1
                                                Ну я в общем вот про это говорил
                                                «I don't think it's possible to make an implementation like CPython as
                                                fast as an engine like V8 that was designed to
                                                be fast above all else. We've come up with some optimizations already
                                                that would simply be too difficult to implement in CPython, and so we
                                                had to discard them. Being a volunteer-run open-source project,
                                                CPython requires somewhat different priorities than V8: CPython places
                                                a heavy emphasis on simplicity, the idea being that a simple, slower
                                                core will be easier for people to maintain in their free time than a
                                                more complicated, faster core. I have high hopes for one of the other Python implementations to
                                                provide a longer-term performance solution designed without the
                                                shackles of C-level backwards compatibility. „
                                                • 0
                                                  Ах это, да читал, но подзабыл. Тогда в основном pypy получается. В общем-то для python в принципе желательны варианты — кому то менее прогрессивный, но более навороченный вариант, кому то CPython.
                                  • +2
                                    Интересно почему в Stackless не реализовали автоматическое переключение между тасклетами при блокирующих IO операциях на уровне сокетов/файлов?

                                    Это в принципе нереально организовать или слишком затратно?

                                    //Видимо не зря я за Erlang взялся…
                                    • 0
                                      В принципе, так делают. Манкипатчат вызовы функций socket и подставляют вместо них «асинхронные» версии. Теоретически ничего этому не мешает, но практически нормальной реализации нет, которая различает блокирующий и неблокирующий режимы, умеет понимать разницу между read(buf, 1024) — «прочитай 1024 или можешь не возвращаться» и «прочитай хоть сколько-нибудь, но не больше 1024».

                                      Что касается Erlang, то это отличный язык, от рождения сделанный асинхронным. Единственная проблема — его применение в коммерческом проекте. Где найти знающих его программистов? С Python попроще будет.
                                      • 0
                                        есть мнение что его можно выучить =)
                                        • +2
                                          Erlang немного не о том. В стаклес у вас вообще одна нить выполнения, только одна, а Erlang умеет задействовать все процессоры, потому что у него не завязано все на одну нить. Ну и вообще не так все просто, как вы себе представили — в Erlang типа нельзя поменять значение «переменной», а потому гонки там и невозможны, а вовсе не из-за того, что нитей нет тупо.
                                          • +1
                                            > Erlang типа нельзя поменять значение «переменной»
                                            Впрочем, это не мешает разработке :)
                                            Там просто по-другому подходишь к задаче: функция разбивается на маленькие локальные единицы смысла, в пределах которых нет необходимости менять значение переменной.
                                      • –1
                                        Александр, это офигенно!
                                        Хочу попробовать, как раз в отпуске может найду часок :)
                                        • –1
                                          забавно наблюдать в очередной как изобретают erlang
                                          • +3
                                            Ох…
                                            Вот у меня тоже главные языки Python и Erlang. И не все так однозначно. У питона есть мощное преимущество в наличии огромного количества библиотек, нормальная поддержка для тех же юникодных строк, и другие инструменты, которые облегчают базовые рутинные операции. В эрланге же намного сильнее основа: и по скорости работы, и по надежности, и даже есть возможность безопасно kill`нуть процесс.
                                          • 0
                                            А насколько он вообще готов к продакшну? Скорость, стабильность, поддержка имеющихся библиотек питона (в т.ч. C-шных), как тот же джанго работает на stackless? Особенно интересует preemptive — не хочется думать о явной передаче управления другим нитям, тем более что и код в уже существующих библиотеках вряд ли об этом думает.
                                            • 0
                                              На самом stackless работает онлайн-игра Eve Online. Самый что ни на есть ацкий продакшн, причём с колоссальными нагрузками. Что касается джанго, ничего сказать не могу — не пробовал. Но принципиально не вижу никаких проблем — должно завестись с пол-оборота.

                                              Preemptive убивает всю полезность stackless. Вы уже не можете быть уверены, что ваши операции атомарны. О передаче управления задумываться не надо вообще, поскольку оно происходит само на любом вводе-выводе. Если речь идёт о web-приложениях, то там этот ввод-вывод постоянно идёт в запросах к кэшам и базам данных. Ничего специально тюнить не надо.

                                              Проблемы с существующими библиотеками могут быть только в одном месте — если они сами делают какой-то ввод-вывод, который может заблокировать процесс. Пока он не будет отдавать управление, остальные микропотоки (которые могли бы выполняться) будут тоже стоять и ждать. Решение — адаптировать каждую используемую библиотеку для stackless — заворачивать сетевой ввод-вывод в сокеты concurrence. Для многих библиотек это уже сделано, для некоторых — ещё нет. В частности, нет реализации SSL-сокетов. Поэтому смотрите по задаче — если используете экзотику, надо её дорабатывать. Если не используете — тогда всё будет работать из коробки.
                                              • +1
                                                Preemptive убивает всю полезность stackless. Вы уже не можете быть уверены, что ваши операции атомарны.
                                                А зачем мне атомарность операций? Просто не надо работать из разных нитей с одними и теми же структурами данных, всё общение между нитями только через каналы. Проблема в том, что я могу гарантировать соблюдение этого для своего кода, но не для используемых библиотек.

                                                В веб-приложениях тоже иногда встречаются тяжёлые запросы в базу, а база это далеко не всегда MySQL — есть ещё NoSQL. Например, данные могут храниться в обычных .json файлах, и тогда этот «тяжёлый запрос в базу» будет выполняться как чтение/запись множества .json-файлов с дополнительными вычислениями. Блокирующие чтение/запись обычных файлов тоже будет манкипатчиться и обрабатываться через epoll?

                                                Кроме того, конкретно у нас встречаются тяжёлые вычислительно задачи, когда требуется просчитать все возможные сочетания кучи элементов — без preemptive очень сложно гарантировать что в других нитях не начнутся проблемы из-за слишком долгого простоя (например, при том же неблокирующем I/O, удалённая сторона может разорвать соединение из-за того, что наша нить не вычитывает данные из буфера ядра).

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

                                                Получается, что пока речь идёт о том, чтобы писать неблокирующий I/O в простом блокирующем стиле на нитях — stackless полностью удовлетворяет наши потребности. Но когда речь заходит о general программировании на нитях, точнее о его простом CSP-шном варианте (а-ля Limbo или Go), когда на каждый чих делается новая лёгкая нить и всё общение между ними делается через каналы — здесь возникает необходимость в preemptive, и, судя по всему, несовместимость с большинством библиотек. Возможно я ошибаюсь, и буду рад, если Вы мне на это укажете, но пока впечатление складывается именно такое.

                                                Что касается Eve Online — я подозреваю, что там stackless используется именно в том качестве, в котором он хорош — для упрощения неблокирующего I/O и без preemptive.
                                                • 0
                                                  Атомарность полезна для исключительно быстрой реализации примитивов синхронизации. if busy == 0: busy = 1 [some-code] busy = 0. Если это не требуется, то конечно preemprive будет работать.

                                                  Про NoSQL вы всё правильно написали. Если обращение к базе по сети, то можно сокет подменить, и всё будет работать. Мы так работаем с Cassandra (она на Thrift). Не будет проблем с CouchDB (он на HTTP). Если надо читать стопицот файлов, то да — тут проблемы будут. Concurrence не умеет файлы через epoll диспетчерить. Это надо специально её этому учить.

                                                  • 0
                                                    Что касается библиотек, то если они внешние по отношению к Python, то в них счётчик инструкций вообще работать не будет, и шедулер не сможет выдернуть оттуда управление. Так что для таких задач даже Preemptive неприемлем.

                                                    В Eve Online stackless используется именно для неблокирующего I/O и без preemptive, насколько я знаю.

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