Использование Python в многопоточном приложении на C++ и настоящая многопоточность в Python

    Все более или менее знающие Python разработчики знают про такую жуткую вещь как GIL. Глобальный блокировщик всего процесса до тех пор пока Python выполняется в одном из потоков. Он даёт потоко-защищённость методами сравнимыми с садизмом, поскольку любая неявная блокировка в многопоточном приложении смерти подобна, всё что опиралось на параллельное выполнение, умирает в мучениях, раз за разом натыкаясь на блокировку GIL.
    Известно что по сей день из-за этого скорбного факта программисты на C++ используют Python-обёртки по большей части лишь в однопоточных приложениях, а программисты на Python пытаются всех убедить, что им и так неплохо живётся.
    Казалось бы, если поток порождён в C++, он не знает ни о каком GIL, используй Python без блокировок и радуйся. Радость разработчика однако закончится уже на втором потоке запросившем область глобальных переменных без блокировки.
    Однако есть путь ведущий к светлому будущему!
    Этот путь был изначально в таком языке как Perl, он же поддерживается в Си-API языка Python и я ума не приложу почему подобный механизм не включен в один из стандартных модулей Python! Способ по сути сводит использование различных под-интерпретаторов Python в разных потоках, причём используя свой GIL для каждого(!!!) без всякого шаманства и магии, просто последовательно вызвав несколько функций и стандартного набора Си-API языка Python!

    Честная многопоточность в Python


    Всё нижеперечисленное основывается на новом GIL, введённом в Python 3.2, отлажено и работает на Python 3.3. Однако для более ранних версий, том же Python 2.7, предлагается использовать то же API, само поведение GIL не так важно, как его запуск из порождённого интерпретатора.
    Итак начнём, нам потребуется основной поток, в котором мы просто всё проинициализируем и запустим некоторое количество потоков, работающих с различным функционалом Python, как родным питоновским, так и написанном на C++. Всё будем делать из C++, будем работать с библиотеками boost::python и boost::thread. Если у вас ещё нет библиотеки BOOST, либо вы используете чистый Си вместо C++, это не страшно, основная работа здесь идёт на Си, а BOOST используется лишь для наглядности и простоты разработки, всё то же самое можно сделать на чистом Си, используя API Python и API ОС для работы с потоками.
    В начале работы с Python нужно проинициализировать интерпретатор, включить механизм GIL и разрешить многопоточность в GIL, сохранив состояние главного потока:
            Py_Initialize();        // инициализация интерпретатора Python 
            PyEval_InitThreads();   // инициализация потоков в Python и механизма GIL
    
            mGilState = PyGILState_Ensure();     // забираем себе GIL сразу для настройки многопоточности
            mThreadState = PyEval_SaveThread();  // сохраняем состояние главного потока и отпускаем GIL
    
            // здесь GIL разблокирован для основного интерпретатора Python и ждёт блокировки из других потоков
    

    Разумеется инициализация подразумевает и освобождение ресурсов, удобнее всего завести класс с конструктором и деструктором, где деструктор восстанавливает состояние потока, освобождает GIL и завершает работу интерпретатора (включая работу под-интерпретаторов):
            // здесь GIL должен быть разблокирован для основного интерпретатора
    
            PyEval_RestoreThread( mThreadState );   // восстанавливаем состояние главного потока и забираем себе GIL
            PyGILState_Release( mGilState );        // отпускаем блокировку GIL с сохранённым состоянием
    
            Py_Finalize();  // завершает работу как основного интерпретатора, со всеми под-интерпретаторами Python
    

    Пока что всё очевидно для всех кто когда-либо работал с GIL из Си-API языка Python. Для основного потока всего лишь требуется выполнять роль диспетчера, не блокируя GIL и не мешая остальным потокам делать свою работу. Вот так примерно должен выглядеть класс целиком:
    class PyMainThread  // специальный класс для основного потока
    {
    public:
        PyMainThread()  // нужно создать экземпляр класса в самом начале работы
        {
            Py_Initialize();        // инициализация интерпретатора Python 
            PyEval_InitThreads();   // инициализация потоков в Python и механизма GIL
    
            mGilState = PyGILState_Ensure();     // забираем себе GIL сразу для настройки многопоточности
            mThreadState = PyEval_SaveThread();  // сохраняем состояние главного потока и отпускаем GIL
    
            // здесь GIL разблокирован для основного интерпретатора Python и ждёт блокировки из других потоков
        }
    
        ~PyMainThread() // по завершении работы нужно корректно освободит ресурсы интерпретатора
        {
            // здесь GIL должен быть разблокирован для основного интерпретатора
    
            PyEval_RestoreThread( mThreadState );   // восстанавливаем состояние главного потока и забираем себе GIL
            PyGILState_Release( mGilState );        // отпускаем блокировку GIL с сохранённым состоянием
    
            Py_Finalize();  // завершает работу как основного интерпретатора, со всеми под-интерпретаторами Python
        }
    
    private:
        PyGILState_STATE mGilState;     // сохранённое состояние GIL
        PyThreadState* mThreadState;    // сохранённое состояние основного потока
    };
    

    Собственно работа в функции main() или её аналоге сводится к следующей схеме:
        PyMainThread main_thread; // начальная инициализация интерпретатора в главном потоке
    
        boost::thread_group group;
    
        // порождаем потоки, каждый выполняет свою работу, взаимодействуя с Python без общего GIL
        for( int id = 1; id <= THREAD_NUM; ++id)
            group.create_thread( ThreadWork(id) );
    
        group.join_all();
    

    Всё, закончили с примитивщиной, народ жаждет магии… ах да, я обещал что её не будет.

    Работа в каждом потоке


    Если мы сейчас попробуем просто сделать в каждом порождённом потоке time.sleep(1) мы получим падение уже на втором потоке.
    Нас спасёт волшебная функция Py_NewInterpreter (!!!), в которой всё бы хорошо, но её использование требует блокировки GIL (!) и это было бы страшно, если бы не тот факт, что GIL приходит и уходит, а под-интерпретатор останется. И уже в нём можно блокировать его GIL сколько угодно, у него самого потоков будет ровно 1 — тот в котором его и создали:
            mMainGilState = PyGILState_Ensure();    // забираем блокировку основного интерпретатора
            mOldThreadState = PyThreadState_Get();  // сохраняем текущее состояние порождённого потока
            mNewThreadState = Py_NewInterpreter();  // создаём в потоке под-интерпретатор
            PyThreadState_Swap( mNewThreadState );  // с этого момента для потока актуален уже под-интерпретатор
    
            mSubThreadState = PyEval_SaveThread();  // не забываем освободить предыдущую блокировку GIL
            mSubGilState = PyGILState_Ensure();     // и заблокировать GIL уже для под-интерпретатора
    

    Это также лучше всего сделать в конструкторе специального класса, а деструкторе соответственно следующий код:
            PyGILState_Release( mSubGilState );         // разблокируем GIL для под-интерпретатора
            PyEval_RestoreThread( mSubThreadState );    // восстанавливаем блокировку и состояние потока для основного интерпретатора
            Py_EndInterpreter( mNewThreadState );       // завершаем работу под-интерпретатора
            PyThreadState_Swap( mOldThreadState );      // возвращаем состояние потока для основного интерпретатора
            PyGILState_Release( mMainGilState );        // освобождаем блокировку GIL для основного интерпретатора
    

    Код всего класса приведён ниже:
    class PySubThread   // класс для работы в каждом порождённом потоке
    {
    public:
        PySubThread()   // нужно для инициализации под-интерпретатора Python в самом начале работы потока
        {
            mMainGilState = PyGILState_Ensure();    // забираем блокировку основного интерпретатора
            mOldThreadState = PyThreadState_Get();  // сохраняем текущее состояние порождённого потока
            mNewThreadState = Py_NewInterpreter();  // создаём в потоке под-интерпретатор
            PyThreadState_Swap( mNewThreadState );  // с этого момента для потока актуален уже под-интерпретатор
    
            mSubThreadState = PyEval_SaveThread();  // не забываем освободить предыдущую блокировку GIL
            mSubGilState = PyGILState_Ensure();     // и заблокировать GIL уже для под-интерпретатора
        }
    
        ~PySubThread()  // по завершении работы потока нужно корректно освободить ресурсы под-интепретатора Python
        {
            PyGILState_Release( mSubGilState );         // разблокируем GIL для под-интерпретатора
            PyEval_RestoreThread( mSubThreadState );    // восстанавливаем блокировку и состояние потока для основного интерпретатора
            Py_EndInterpreter( mNewThreadState );       // завершаем работу под-интерпретатора
            PyThreadState_Swap( mOldThreadState );      // возвращаем состояние потока для основного интерпретатора
            PyGILState_Release( mMainGilState );        // освобождаем блокировку GIL для основного интерпретатора
        }
    
    private:
        PyGILState_STATE mMainGilState;     // состояние GIL основного интерпретатора Python
        PyThreadState* mOldThreadState;     // состояние текущего потока для основного интерпретатора
        PyThreadState* mNewThreadState;     // состояние потока для порождённого под-интерпретатора
        PyThreadState* mSubThreadState;     // сохранённое состояние потока при разблокировке GIL
        PyGILState_STATE mSubGilState;      // состояние GIL для порождённого под-интерпретатора Python
    };
    

    Как видите работа по инициализации в каждом потоке уже не столь тревиальна и примитивна, как в основном потоке. Однако мы имеем полный PROFIT для каждого потока. Пусть в каждом отдельном под-интерпретаторе нам придётся заново импортировать модули, однако мы получаем почти полную изолированность данных Python для каждого потока!

    Тестируем результат


    Итак, давайте проверим, насколько мы правы. Давайте для полноты ощущений заведём свой модуль на Python написанный на C++ и предоставляющи функцию аналог time.sleep:
    #include <boost/python.hpp>
    #include <boost/thread.hpp>
    
    using namespace boost::python;
    using namespace boost::this_thread;
    using namespace boost::posix_time;
    
    void wait( double sec ) // функция ожидания, аналог стандарному time.sleep в Python
    {
        int msec = static_cast<int>( sec * 1000 );  // переводим в миллисекунды
        sleep( millisec( msec ) );                  // спим указанный в секундах период
    }
    
    BOOST_PYTHON_MODULE( ctimer ) // используем boost::python и создаём модуль ctimer
    {
        def( "wait", wait, args("sec") );   // в Python это будет ctimer.wait(sec)
    }
    

    Собираем DLL и переименовываем в модуль Python ctimer.pyd, если мы под Windows. Полученный модуль ctimer подкладываем для выполнения основного приложения. Будем использовать ctimer.wait наряду со стандартным time.sleep.
    Заводим класс-функтор для работы в каждом отдельном потоке:
    class ThreadWork    // тестовый класс-функтор для передачи в экземпляр boost::thread
    {
    public:
        ThreadWork( int id )    // сохраним порядковый номер запущенного потока
            : mID( id )
        {
        }
    
        void operator () ( void )   // собственно выполняемая работа в каждом потоке
        {
            cout << "Thread#" << mID << " <= START" << endl;
    
            PySubThread sub_thread; // здесь мы порождаем под-интерпретатор Python
    
            for( int rep = 1; rep <= REPEAT_TIMES; ++rep )
            {
                // работаем с модулем написаном на Python
                cout << "Thread#" << mID << " <= Repeat#" << rep << " <= import time; time.sleep(pause)" << endl;
    
                object time = import( "time" );     // import time
                time.attr( "sleep" )( PAUSE_SEC );  // time.sleep(pause)
    
                // работаем с модулем написаном на C++
                cout << "Thread#" << mID << " <= Repeat#" << rep << " <= import ctimer; ctimer.wait(pause)" << endl;
    
                object ctimer = import( "ctimer" ); // import ctimer
                ctimer.attr( "wait" )( PAUSE_SEC ); // ctimer.wait(pause)
            }
    
            cout << "Thread#" << mID << " <= END" << endl;
    
            // здесь под-интерпретатор Python завершит свою работу
        }
    
    private:
        int mID;    // порядковый номер при запуске потоков
    };
    

    Запускаем приложение и радуемся! Потоки параллельно работают с модулями на Python, в каждом потоке отдельно болтаются маленькие под-питончики, которые заблокировали каждый свой GIL и совершенно свободно работают все вместе не мешая друг другу.
    Ура, товарищи!

    Ссылка на проект MSVS 2012 с исходниками (целых два .cpp файла) и собранными DLL и EXE для Python 3.3 x64 можно скачать здесь (290 KB)

    Полезные ссылки


    Работа с под-интерпретатором Python
    API для работы с потоками, интерпретатором и GIL
    Документация по Boost.Python
    Документация по Boost.Thread
    Share post

    Similar posts

    Comments 49

      +3
      Согласен, GIL — просто ужас. С ним я бился, нашел еще несколько проблем связанные с ним (например unix сигналы) и пришел к старому доброму fork. Теперь если нужно что-то распаралелить на python — создаю несколько процессов с одним потоком. С одной стороны затратно, с другой — лучшая изоляция, больше безопасности, проблема в одном потоке не влияет на другой.
        0
        Это здорово! GIL уже останавливал от использования python в качестве встроенного скриптового движка. А как там насчет накладных расходов? Насколько неэффективным будет создать к примеру 50 интерпретаторов в приложении?
          0
          Если подразумевается 50 интерпретаторов для 50 потоков, то это будет конечно медленнее, поскольку 50-ядерных процессоров пока в продаже не так много. Тем не менее заменив сейчас в тестовом проекте THREAD_NUM на 50, всё благополучно завершилось, выполнялось всё параллельно.
          Единственное что каждый import в каждом отдельном под-интерпретаторе будет выполнен заново, поэтому нужно стараться не импортировать в каждом потоке по гигабайту или то что импортируется по несколько минут.
          0
          Важно! В C++ приложении не будет запущен никакой отдельный процесс python.exe, точно так же никакой запуск под-интерпретатора в потоках не породит никаких процессов питона!
          Можно смело использовать Python-модули из C++ кода в полной уверенности, что это будет один-единственный процесс.
          Под-интерпретаторы изолированы почти полностью, единственное что взаимодействие с ОС будет разумеется из одного процесса, текущего, это следует учитывать.
            0
            Интересует вопрос производительности. Насколько создание таких потоков медленне потоков, созданных в самом Питоне? Сколько памяти уходит на один поток с импортированным, скажем, модулем logging?
              –2
              Любая скорость питоновых потоков нивелируется 94479365439678430697207586230756403765084760437643087606**4837564307964306743068740387564035764057640538754035764075 где-нибудь в коде треда.
              Если используете большие цифры и хотите предсказуемости, то либо форк, либо вот такая техника как тут.
                0
                Потоки созданные в самом питоне через модуль threading используют один общий GIL, в один момент времени выполняется строго один из них. С удовольствием могу померять насколько это медленнее параллельного выполнения, если действительно сомневаетесь.
                  0
                  Я понимаю, чем ваш метод принципиально отличается от создания потоков из Питона. Мне интересно, насколько это затратно, создавать отдельный интерпретатор Питона.
                    0
                    Копирование пустого интерпретатора функцией Py_NewInterpreter у меня заняло около 200 мс в среднем, один раз на старте потока. Что разумеется мелочи для инициализации приложения, в сравнении с тем, что для Python версии ниже 3.2 мы получим блокировку вплоть до завершения работы потока, что для длительных операций приводит параллельное исполнение к линейному. В Python 3.2+ разница уже не так заметна, благодаря новой схеме GIL, однако всё равно порядка 2 сек разницы в пользу под-интерпретаторов при условиях: 4 потока, 10 повторов, 0,5 сек. паузы.
                      0
                      200 мс совсем не кисло. У меня старт и завершение полноценного интерпретатора занимает примерно 20 мс.
                        –1
                        Хм, попробуйте обогнать моё приложение своим скриптом, при условии что они оба будут делать одинаковую работу.
                        Разумеется оба должны быть многопоточными и оба должны параллелить некоторый ряд действий.
                        Уверен, что ни при каких прочих равных модуль threading не сможет сравниться с нативными потоками, где делается аналогичная работа в уже подготовленных для работы интерпретаторах параллельно без общего GIL.
                          +1
                          При чем тут threading. Я же про старт полноценного процесса Питона говорю. А форк должен быть еще быстрее.
                +4
                Ну а смысл? Как я вижу, никакие преимущества многопоточности (та же разделяемая память) тут не удастся применить. Ну разве что накладные расходы немного поменьше.
                  0
                  Почему не даёт? Та же разделяемая память вне питона вполне видна. Многопоточность даёт честное параллельное выполнение. То что под-интерпретаторы Python изолированы друг от друга — скорее плюс чем минус. Учитывая что физически это один процесс можно сделать модуль на C++ который даёт потокобезопасный доступ к общим переменным.
                  Собственно смысл в параллельном выполнении и есть. Все остальные плюшки единого процесса остаются.
                    +1
                    Т.е. профит может проявиться только если работу с общими данными реализовать на стороне C? Если работы с общими данными не требуется, проще процессы запускать же?
                      0
                      Межпроцессное взаимодействие на порядок сложнее чем внутрипроцессное между потоками. Проще запустить, но сложнее согласовать и завершить.
                  0
                  Этот путь был изначально в таком языке как Perl, он же поддерживается

                  В Perl потоки не имеют никакого преимущества перед fork() (под Linux). Т.к. «общие» данные на самом деле не общие, а синхронизируются между потоками через сокеты итд.
                    0
                    Ну здесь на самом деле если не учитывать слой C++ интерпретаторы между собой никак взаимосвязаны, так что решение аналогичное. Но то что на уровне C++ можно устроить диспетчер обмена данными между потоками, конечно плюс.
                      0
                      синхронизируются между потоками через сокеты итд
                      really?
                        0
                        в общем, Вы ошибаетесь )
                      +6
                      Оно так не работает.
                      GIL один на процесс, а не на субинтерпретатор.
                      Лежит с ./Python/ceval.c как

                      static PyThread_type_lock pending_lock = 0; /* for pending calls */
                      static long main_thread = 0;
                      /* This single variable consolidates all requests to break out of the fast path
                      in the eval loop. */
                      static _Py_atomic_int eval_breaker = {0};
                      /* Request for dropping the GIL */
                      static _Py_atomic_int gil_drop_request = {0};
                      /* Request for running pending calls. */
                      static _Py_atomic_int pendingcalls_to_do = {0};
                      /* Request for looking at the `async_exc` field of the current thread state.
                      Guarded by the GIL. */
                      static int pending_async_exc = 0;

                      Ваш код работал потому что в примере вы не вызывали сложный код написанный на Питоне.
                      Переключения GIL не было — вот и ничего не ломалось.
                        0
                        Пробовали на Python 3.2+?
                        Секунду, если он внутри и переключится, то для PyThreadState уже нового под-интерпретатора, и вернётся на выполнение к нему же. Кто сказал что переключения не было? С Python 3.2 переключения идут уже по времени, во время sleep могло быть переключение.
                        Действительно ли было падение, если да, то покажите код, на котором схема приведённая выше не работает.
                          0
                          Надо же, я даже испугался, если бы не одно но:
                          > в каждом потоке используется PyGILState_Ensure в начале выполнения и отпускается уже только в самом конце, если бы GIL был глобален, ничего бы не получилось, все бы зависли.
                          На самом деле ключевое значение здесь PyGILState_STATE приводящее к пониманию когда берётся состояние глобальное, а когда нет. Значение UNLOCKED приходящее в порождённые потоки позволяет зажать GIL по-потоково для под-интерпретаторов.
                            +1
                            Еще раз: GIL не лежит в PyInterpreterState. Он в static variables, которые общие для всего процесса.

                            Переключение GIL происходит в PyEval_EvalFrameEx.

                            Пока вы не начали запускать питоновский байткод — переключение GIL не происходит. И ничего не ломается.
                              +3
                              Не всё можно понять глядя на статичный код. Я бы крайне рекомендовал вам заглянуть в пошаговую отладку в данном приложении. Вам, как человеку любознательному, было бы крайне полезно узнать, что после выполнения макроса Py_BEGIN_ALLOW_THREADS (аналог выполняется в конце конструктора PyMainThread) значение возвращаемое запросом на GIL через PyGILState_Ensure будет PyGILState_UNLOCKED.
                              Это фактически означает, что для каждого потока в отдельности разрешается хапнуть свой PyGILState_STATE одновременно.
                              Представляете какая красота, одни и те же функции и два поведения. Фактически ваш метод анализа кода даёт сбой именно в этот момент.
                              Эта маленькая тонкость даёт возможность запустить одновременно N под-интерпретаторов со своим PyThreadState, для которого в каждом потоке делается фактически свой запрос на GIL.
                                +1
                                И снова: GIL в Питоне один, все потоки запускающие питоновский код разделяют владение им.
                                subinterpreters все работают с этим единственным GIL.
                                PyGILState_Ensure() из PyMainThread возвращает PyGILState_UNLOCKED потому что вызывается сразу после Py_Initialize и, естественно, GIL никем еще не захвачен.

                                PyThreadState у каждго субинтерпретатора свой (как и у каждого потока). При этом GIL общий, одновременно запущен только один поток. Остальные спят, пытаясь плучить блокировку.
                                  0
                                  Да нет же, PyGILState_Ensure() который вызывается самым первым в конструкторе PyMainThread как раз возвращает PyGILState_LOCKED. Как раз захватывая GIL. После этого я его в главном потоке отпускаю аналогом Py_BEGIN_ALLOW_THREADS уже следующей инструкцией.
                                  Во всех порождённых потоках вызов PyGILState_Ensure() возвращает уже PyGILState_UNLOCKED — причём ОДНОВРЕМЕННО. Без этой операции в потоке нельзя даже получить текущее состояние потока, нельзя сделать PyThreadState_Swap. Я его делаю сразу после PyGILState_Ensure, без этого получите падение.
                                  После того как я завершаю работу по созданию под-интерпретатора, я подменяю состояние потока и освобождаю предыдущий hold. После чего спокойно делаю PyGILState_Ensure уже на новое состояние потока PyThreadState.
                                  Далее изоляция происходит уже на уровне под-интерпетаторов. GIL действительно общий, уж простите что обозвал всё то что происходит множеством GIL, на самом деле всё это просто хитрый запуск множества изолированных под-интерпретаторов. Я не понимаю где это может не сработать, если есть нерабочий пример, прошу показать.
                                    0
                                    Хорошо. Есть несколько интерпретаторов, разделяющих GIL.
                                    Потоки взаимно им блокируются, то есть работают не параллельно.
                                    Так в чем профит?
                                      0
                                      Вообще-то потоки работают на 4 разных ядрах у меня на Core i5, ладно отлаживаться, запускать приложение пробовали?
                                        0
                                        В общем, чем именно блокируется выполнение нативных потоков? Кроме как при импорте модулей, запрос к глобальным переменным нигде не происходит, там мы попадаем в мьютекс и спокойно всё получаем. Далее всевозможное API фактически уже не использует и не проверяет GIL.
                                        PyGILState_Ensure мы делаем перед выполнением потока, import вполне можно сделать однажды на поток.
                                        Собственно снова вопрос, что мешает потокам порождённым через boost::thread выполнятся параллельно?
                                        Ничего.
                                        Dict у каждого под-интерпретатора свой, обращение к нему безопасно для каждого из своего потока.
                                          0
                                          svetlov видимо из личной скромности не стал говорить, что он Python Core Developer ;)
                                            0
                                            Я в курсе, спасибо, но даже разработчик ядра питона может пару раз сказать вещи, которые расходятся с действительностью. Всё вышесказанное проверяется простой отладкой.
                                              +1
                                              В коде я увидел, что субинтерпретаторы захватывают GIL перед использованием (отчего-то два раза, но это не принципиально).

                                              После этого код в ThreadWork::operator() выполняется уже не параллельно с другими ThreadWork (GIL-то один).

                                              Для демонстрации вызов ctimer.wait() можно заменить на boost::this_thread::sleep()

                                              Опять же для усугубления эффекта рекомендую увеличить паузу с полусекунды раз в 10-100.
                            +3
                            И не понятно, чем это лучше этого: docs.python.org/2/library/multiprocessing.html
                              0
                              Тем, что процесс один, и из приложения на C++ не создаётся ни одного процесса Python. По сути получаете приложение, свободно использующее по интерпретатору в каждом потоке, однако при этом кроме Вашего процесса ни одного не будет создано. Легко использовать общие данные, согласовать между собой задания потоков и завершать их куда проще чем процессы.
                                +3
                                Не понимаю такой боязни создания процессов. POSIX-системы их создают на каждый чих, и всё работает отлично.

                                Видимо, это у Вас от какой-то не POSIX операционки такой страх перед процессами, я прав?

                                (Это кроме того, что для решения этой задачи есть Pypy, stackless и Twisted.)
                                  0
                                  По роду деятельности я должен поддерживать работоспособность на Windows. Там всё довольно печально. Поэтому же решение с fork меня не устраивало. Сейчас я уже этому даже рад, моё решение кроссплатформенное, использует один процесс и изолирует память интерпретатора (под-интерпретаторов) между потоками. Почти идеал. Осталось только найти косяки.
                                    0
                                    Для windows — да, это вариант.

                                    А для Linux — fork() давно уже не копируюет всю память процесса — происходит copy on write, почти так же быстро как создание потоков. Единственный недостаток — данными нужно как-то обмениваться между процессами…
                                      0
                                      Я бы согласился, если бы не одно но. Что по-вашему делает эта функция: PyOS_AfterFork
                                      Боюсь для C++ приложения использующего Python в общем случае можно упустить такую мелочь как
                                      «to update some internal state after a process fork; this should be called in the new process if the Python interpreter will continue to be used. If a new executable is loaded into the new process, this function does not need to be called»
                                      Понятно что для multiprocessing это не актуально, а вот для fork боюсь тут надо разобраться, что это за «some internal state after a process fork».
                                      Не хотелось бы наступить на невидимые грабли с внутренней кухней данных в Python.
                              0
                              Вопрос от чайника. По собственным ощущениям чистый Python C API мне показался очень мозгодробильным с его Py_BuildValue(), рефкаунтингом, макросами для описания типов и т.д. Дело привычки, впрочем. Все это, кстати, и на C++ работает без экстернов. В Boost все выглядит намного наглядней и изящней. И у меня вопрос, накладывает ли Boost какие-либо ограничения? Исходники Blender'а (C++), например, используют чистый API без обвесок. (Мне это нужно для ускорения расчетов матана модулем, потому мне GIL как бы и удобнее, если OpenCL нет в системе).
                                0
                                Если нужно быстро считать — может вам cython.org/ подойдет?
                                А так — нет, никаких специфических boost ограничений нет
                                  0
                                  А ещё Numba.
                                    0
                                    Я в этом особенного смысла не вижу. Если я с PyArg_ParseTuple() я могу конвертировать данные в нативные типы и дальше иметь полный контроль над байтиками памяти в стеке и куче, и все возможности сишных библиотек по обходу графов, и т.д., после макроса Py_BEGIN_ALLOW_THREADS считать матан в несколько потоков, и там же, если че, сгенерировать нужное исключение, и потом результат с помощью Py_BuildValue() передать обратно в Python. В Boost, мне показалось, приятней работа с кастомными типами (наследование структур и т.д.) и это все лучше, чем cython.
                                    +1
                                    Boost.Python замечательная библиотека. Единственное ограничение: постарайтесь обойтись без глобальный и статических переменных типа boost::python::object и его наследников. Иначе после Py_Finalize в деструкторе ~object() вы поимеете кучу неприятных последствий. В целом Boost.Python показал себя замечательной библиотекой, по сути ничего лишнего с одной стороны, даже mpl там очень в тему, через fusion красиво передаются аргументы и всё это очень здорово упрощает конечный код.
                                    По сути на стороне C++ всё сводится к тройке: import(), attr(), extract().
                                    Единственное где может ещё потребоваться Си-API — при извлечения информации об исключении из Python, когда поймаете catch( boost::python::error_already_set& ) { }, но это пожалуй единственное явное белое пятно в Boost.Python при работе с Python из C++.
                                    +1
                                    Как питонист «со стажем» недавно открыл для себя Groovy, в котором легко решаются задачи, требующие тяжеловесной работы с тредами. Синтаксис после питона дается очень легко. Если пишете что-то маленькое, но полезное, и на питоне упирающееся в GIL, попробуйте. :)

                                    Ссылочка для получения представления: groovy.codehaus.org/Concurrency+with+Groovy
                                      0
                                      А для чего вы из С++ используете Python? Это какие то внутренние нужны, типа автотестирования или прям что то в продакшен?
                                        0
                                        Прямо в продакшн: расширения функционала в виде классов и методов на Python.
                                        Удобно: не требует компиляции и подцепляется простым перезапуском.
                                        Опять же можно через Python подключиться и в режиме командной строки узнать много интересного о состоянии сервиса.
                                        По сути сводим слой бизнес-логики к скриптовой обвязке над функциональным ядром скомпилированным на С++.
                                        +3
                                        Отличная статья, ставлю плюс! Получился такой missing manual по управлению GIL.

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

                                        Помните эпоху распространения Win XP? Уверен, у многих был тогда один процессор с одним ядром. И вы запускали на нём программы, которые, казалось работают параллельно. Фантиастика! Программистам того времени приходилось писать грамотный код, чтобы не «убить» компьютер. А что теперь? Каждый, кто не в состоянии подумать об архитектуре программы, плодит десятки (сотни) потоков, на выполнение любой псевдо-параллельной задачи. Нужно посылать запросы, когда пользователь кликнул мышью, ок, давайте сделаем отдельный поток, который будет стоять 99.999% времени! Спрашивается, зачем? Не жалко ресурсов?! У вас не так много ядер, как выдумаете.

                                        Так вот. GIL учит писать хорошие программы, используя волокна и сопрограммы. Нужно выполнить сложную вычислительную задачу? Используйте CUDA, напишите её на Fortran, или создайте отдельный процесс, разверните задачу в PiCloud. При необходимости используйте межпроцессное взаимодействие и Google Compute Engine. Но не ругайте GIL, только потому, что не пробовали использовать шаблон Reactor и вычислять penalty для приостановки потока.
                                          +1
                                          >>Помните эпоху распространения Win XP? Уверен, у многих был тогда один процессор с одним ядром. И вы запускали на нём программы, которые, казалось работают параллельно. Фантиастика! Программистам того времени приходилось писать грамотный код, чтобы не «убить» компьютер

                                          Что вы имеете ввиду? Эпоха XP как раз принесла разделение адресных пространств kernel и user и уронить систему стало намного сложнее.

                                        Only users with full accounts can post comments. Log in, please.