company_banner

Действительно ли Python GIL уже мертв?

Автор оригинала: Anthony Shaw
  • Перевод
Всем привет! Уже в следующий понедельник начинаются занятия в новой группе курса «Разработчик Python», а это значит, что у нас есть время для публикации еще одного интересного материала, чем мы сейчас и займемся. Приятного прочтения.



В далеком 2003 году Intel выпустил новый процессор Pentium 4 “HT”. Этот процессор разгонялся до 3ГГц и поддерживал технологию гиперпоточности.



В последующие годы Intel и AMD боролись за достижение наибольшей производительности настольных компьютеров, увеличивая скорость шины, размер кэша L2 и уменьшая размер матрицы для минимизации задержки. В 2004 году на смену модели HT с частотой 3ГГц пришла 580 модель “Prescott” с разгоном до 4ГГц.



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

Процессор вашего настольного ПК сегодня выдает 4ГГц? Маловероятно, поскольку путь к повышению производительности в конечном итоге лежал через повышение скорости шины и увеличение количества ядер. В 2006 году Intel Core 2 заменил Pentium 4 и имел гораздо более низкую тактовую частоту.

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

У Python 2.5 имелось одно серьезное ограничение, когда речь заходила об использовании Intel Core 2 или AMD Athlon X2.
Это был GIL.

Что такое GIL?


GIL (Global Interpreter Lock – глобальная блокировка интерпретатора) – это булевое значение в интерпретаторе Python, защищенное мьютексом. Блокировка используется в основном цикле вычисления байткода CPython, чтобы установить, какой поток в данный момент времени выполняет инструкции.

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

GIL упрощает многопоточное программирование на Python.



GIL также говорит нам о том, что в то время, как CPython может быть многопоточным, только один поток в любой момент времени может выполняться. Это означает, что ваш четырехъядерный процессор делает примерно это (за исключением синего экрана, надеюсь).

Текущая версия GIL была написана в 2009 году для поддержки асинхронных функций и осталась нетронутой даже после множества попыток убрать ее в принципе или изменить требования к ней.

Любое предложение убрать GIL было обосновано тем, что глобальная блокировка интерпретатора не должна ухудшать производительность однопоточного кода. Тот, кто пробовал включать гиперпоточность в 2003 году, поймет, о чем я говорю.

Отказ от GIL в CPython


Если вы хотите действительно распараллелить код на CPython, вам придется использовать несколько процессов.

В CPython 2.6 модуль multiprocessing был добавлен в стандартную библиотеку. Мультипроцессная обработка (multiprocessing) маскировала собой порождение процессов в CPython (каждый процесс со своей собственной GIL).

from multiprocessing import Process

def f(name):
    print 'hello', name

if __name__ == '__main__':
    p = Process(target=f, args=('bob',))
    p.start()
    p.join()


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

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

У мультипроцессной обработки есть один главный недостаток. Она несет значительную вычислительную нагрузку, которая отражается как на времени обработки, так и на использовании памяти. Время запуска CPython даже без no-site составляет 100-200 мс (загляните на https://hackernoon.com/which-is-the-fastest-version-of-python-2ae7c61a6b2b, чтобы узнать больше).

В итоге у вас может быть параллельный код на CPython, но вам все еще нужно тщательно распланировать работу длительных процессов, которые совместно используют несколько объектов.

Другой альтернативой может являться использование стороннего пакета, такого как Twisted.

PEP554 и смерть GIL?


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

Что если есть путь лучше?
Ключ к обходу GIL кроется в имени, глобальная блокировка интерпретатора является частью глобального состояния интерпретатора. Процессы CPython могут иметь несколько интерпретаторов и, следовательно, несколько блокировок, однако эта функция используется редко, поскольку доступ к ней есть только через C-API.

Одной из особенностей CPython 3.8, является PEP554, реализация субинтерпретаторов и API с новым модулем interpreters в стандартной библиотеке.

Это позволяет создавать несколько интерпретаторов из Python в рамках одного процесса. Еще одно нововведение Python 3.8 заключается в том, что все интерпретаторы будут иметь свои собственные GIL.



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

Подобно мультипроцессной обработке, совместное использование интерпретаторами объектов заключается в их сериализации и использовании формы IPC (сеть, диск или общая память). Существует много способов сериализации объектов в Python, например, модуль marshal, модуль pickle или более стандартизированные методы, такие как json или simplexml. Каждый из них имеет свои плюсы и минусы, и все они дают вычислительную нагрузку.

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



API для этого все еще разрабатывается, но оно, вероятно, будет выглядеть примерно так:

import _xxsubinterpreters as interpreters
import threading
import textwrap as tw
import marshal

# Create a sub-interpreter
interpid = interpreters.create()

# If you had a function that generated some data
arry = list(range(0,100))

# Create a channel
channel_id = interpreters.channel_create()

# Pre-populate the interpreter with a module
interpreters.run_string(interpid, "import marshal; import _xxsubinterpreters as interpreters")

# Define a
def run(interpid, channel_id):
    interpreters.run_string(interpid,
                            tw.dedent("""
        arry_raw = interpreters.channel_recv(channel_id)
        arry = marshal.loads(arry_raw)
        result = [1,2,3,4,5] # where you would do some calculating
        result_raw = marshal.dumps(result)
        interpreters.channel_send(channel_id, result_raw)
        """),
               shared=dict(
                   channel_id=channel_id
               ),
               )

inp = marshal.dumps(arry)
interpreters.channel_send(channel_id, inp)

# Run inside a thread
t = threading.Thread(target=run, args=(interpid, channel_id))
t.start()

# Sub interpreter will process. Feel free to do anything else now.
output = interpreters.channel_recv(channel_id)
interpreters.channel_release(channel_id)
output_arry = marshal.loads(output)

print(output_arry)


В этом примере используется NumPy. Массив numpy отправляется по каналу, он сериализуется с помощью модуля marshal, затем субинтерпретатор обрабатывает данные (на отдельном GIL), поэтому здесь может возникнуть проблема распараллеливания, связанная с ЦП, что идеально подойдет для субинтерпретаторов.

Это выглядит неэффективно


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

В PEP574 представлен новый протокол pickle (v5), который поддерживает возможность обработки буферов памяти отдельно от остальной части потока pickle. Что касается больших объектов данных, то сериализация их всех на одном дыхании и десериализация из субинтерпретатора добавит большое количество накладных расходов.

Новый API может быть реализован (чисто гипотетически) следующим образом —

import _xxsubinterpreters as interpreters
import threading
import textwrap as tw
import pickle

# Create a sub-interpreter
interpid = interpreters.create()

# If you had a function that generated a numpy array
arry = [5,4,3,2,1]

# Create a channel
channel_id = interpreters.channel_create()

# Pre-populate the interpreter with a module
interpreters.run_string(interpid, "import pickle; import _xxsubinterpreters as interpreters")

buffers=[]

# Define a
def run(interpid, channel_id):
    interpreters.run_string(interpid,
                            tw.dedent("""
        arry_raw = interpreters.channel_recv(channel_id)
        arry = pickle.loads(arry_raw)
        print(f"Got: {arry}")
        result = arry[::-1]
        result_raw = pickle.dumps(result, protocol=5)
        interpreters.channel_send(channel_id, result_raw)
        """),
                            shared=dict(
                                channel_id=channel_id,
                            ),
                            )

input = pickle.dumps(arry, protocol=5, buffer_callback=buffers.append)
interpreters.channel_send(channel_id, input)

# Run inside a thread
t = threading.Thread(target=run, args=(interpid, channel_id))
t.start()

# Sub interpreter will process. Feel free to do anything else now.
output = interpreters.channel_recv(channel_id)
interpreters.channel_release(channel_id)
output_arry = pickle.loads(output)

print(f"Got back: {output_arry}")

Это выглядит шаблонно


По сути, этот пример построен на использовании API низкоуровневых субинтерпретаторов. Если вы не использовали библиотеку multiprocessing, некоторые проблемы покажутся вам знакомыми. Это не так просто, как потоковая обработка, вы не можете просто, скажем, запустить эту функцию с таким списком входных данных в отдельных интерпретаторах (пока).

Как только этот PEP объединится с другими, я думаю, мы увидим несколько новых API в PyPi.

Сколько накладных расходов имеет субинтерпретатор?


Короткий ответ: Больше, чем поток, меньше, чем процесс.
Длинный ответ: Интерпретатор имеет свое собственное состояние, потому ему нужно будет клонировать и инициализовать следующее, несмотря на то, что PEP554 упрощает создание субинтерпретаторов:

  • Модули в пространстве имен __main__ и importlib;
  • Содержимое словаря sys;
  • Встроенные функции ( print(), assert и т.д.);
  • Потоки;
  • Конфигурацию ядра.


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

Как насчет asyncio?


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

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

Звучит круто, заверните и мне!


Ну, не совсем.
Поскольку CPython так долго работал на одном интерпретаторе, многие части базы кода используют “Runtime State” вместо “Interpreter State”, поэтому если бы PEP554 был введен уже сейчас, проблем все равно было бы много.

Например, состояние сборщика мусора (в версиях 3.7<) принадлежит среде выполнения.

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

Другая проблема заключается в том, что есть некоторые «глобальные» переменные, которые порядком задержались в базе кода CPython наряду с множеством расширений на С. Поэтому, когда люди внезапно начали правильно распараллеливать свой код, мы разглядели некоторые проблемы.

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

Короче говоря, есть еще много проблем, которые надо решить.

Заключение: GIL правда уже не актуален?


GIL по-прежнему будет использоваться для однопоточных приложений. Поэтому даже при следовании PEP554 ваш однопоточный код внезапно не станет параллельным.
Если вы хотите писать параллельный код в Python 3.8, у вас будут проблемы распараллеливания, связанные с процессором, но это и билет в будущее!

Когда?


Pickle v5 и совместное использование памяти для мультипроцессной обработки скорее всего будут в Python 3.8 (Октябрь 2019 года), а субинтерпретаторы появятся между версиями 3.8 и 3.9.
Если у вас есть желания поиграться с представленными примерами, то я создал отдельную ветку со всем необходимым кодом: https://github.com/tonybaloney/cpython/tree/subinterpreters.

А что вы думаете по этому поводу? Пишите свои комментарии и до встречи на курсе.
OTUS. Онлайн-образование
785,52
Цифровые навыки от ведущих экспертов
Поделиться публикацией

Похожие публикации

Комментарии 48

    0
    Slain — это не «мертв», а «побежден».
      +1
      Если еще точнее, то «убитый», либо «умерщвленный». Но нам показалось, что «мертв» более точно передает смысл текста.
      +2

      Я чего-то не понял, почему рисунок говорит про четыре ядра? Их же явно пять — с 0 по 4.

        +3

        Там еще три за обновлениями ушли.

        +3
        Выглядит костыльненько, но лучшем чем с процессами.

        Однако, как по мне, так если нужно многопоточная обработка, то пожалуй лучше на Go написать.
          +3

          Попытка принести shared state с concurrency обещает столько веселья, что только запасайся попкорном. Раньше GIL решал все проблемы — у вас просто не могло быть гонки условий. Выпиливание GIL потребует таких невероятных изменений в системе типов, что это перестанет быть питоном.


          Решений с shared mutability ровно два: запретить mutability или запретить shared. Любое из этих решений сделает из питона инвалида, и даже самое изящное из существующих (ownership/borrow model в Rust), будучи принесённой в Python, сделает из него что угодно, кроме питона на котором легко писать.

            0
            Прочитав комментарий я как раз наоборот подумал. Скорее не borrow, а move из Rust отлично решит проблему. любые переменные либо должны быть immutable, либо реализовать mutex иначе будут перемещены при передаче в субинтерпретатор. Но передавать надо указатель на shared memory, а не передавать байты
              +6

              borrow — это такой маленький синтаксический сахар для такого:


              a = foo(a, ...)

              Функция "взяла" объект и вернула. Вместо этого компилятор сам понимает "вернула" без дополнительных байтов на стеке, просто по лексическому контексту.


              … А вот как только вы сделаете "передать указатель на shared memory", вот тут-то вас и ждут драконы.


              Потому что вы не можете контролировать жизнь объекта по ссылке. Вы не можете запретить асинхронный доступ к объекту, вы не можете запретить использовать объекты после их удаления. Если всё это утыкать reference counter'ами (у питона они уже есть), то станет чуть лучше в смысле use after free, но мутекс на каждую переменную — это смерть однопоточной производительности. От слова "совсем", потому что на современных процессорах атомарные операции инвалидируют кеши соседей (других процессоров) межпроцессорным прерыванием. Каждый раз, когда вы трогаете переменную, ваш процессор бьёт в поддых каждого из соседей и говорит "а я изменил переменную". Разумеется, это не прокатит.


              А если вы сделаете часть переменных локальными, а часть за мутексами, то всё станет плохо — Питон не может заставить вас использовать только мутексные переменные.


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


              … Короче, плохо. И то, сколько людей пало пытаясь заменить GIL на что-то — явное тому доказательство.

              0

              Простите, я не совсем в теме, но какая связь между многопоточностью и системой типов? Почему нельзя отдать все заботы по синхронизации на откуп программисту? Вы можете писать однопоточный код и совсем не задумываться о синхронизации, а если что-то с concurency — использовать блокировки и синхронизацию (мьютексы, критические секции, семафоры итп) или вообще lock-free, если можно. В общем, как это делают на других языках.

                +2

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


                В языке с контролем за эффектами (всякие хаскели и идрисы) я могу писать код в монаде STM и получить гарантии от компилятора, что software transactional memory будет работать как ожидается, например.


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


                А языке с rank-2 polymorphism вы можете гарантировать, что вы не будете пользоваться ресурсом вне определенного скоупа типа транзакции, мьютекса и тому подобного. И, кстати, заодно можете гарантировать детерминированность параллельных вычислений.


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

                  +3

                  Так с типами то же самое. Типы — это запрет на определённые операции. Если бы у нас был безтиповый язык, то мы бы могли вызвать (как функцию) true, взять пятый элемент числа 1 и разделить "hello" на "world".


                  Понятно, что тут есть элемент фанбойства, но в Rust система типов и система lifetimes (это ещё одна система ограничений, но не типов, со своими правилами), даёт возможность явно защитить программиста от совершения многопоточных ошибок.


                  Если вы отдаёте на откуп программисту самому разбираться со всем этим без защиты от ошибок, то это эквивалентно безтиповому языку с goto из середины одной функции в середину другой. Можно, но чревато.

                  +1

                  Растовский подход далеко не самый изящный. Полноценные аффинные типы, rank-2 polymorphism и гарантированная чистота таки поизящнее и помощнее.

                    0
                    А какой язык это реализует?
                      +1

                      Всё вместе — только Idris (но там аффинные типы не очень полноценные, ибо единственная поддерживаемая multiplicity — 1, что, впрочем, не сильно отличается от раста, в котором она, как я понимаю, всегда ≤1). Но на идрисе никто не пишет.


                      Последние два — хаскель, и у меня есть впечатление, что если есть rank-2 и контроль за эффектами, то аффинные типы не очень нужны, но это ещё надо доказать формально. Ну и скоро завезут линейные типы, да.

                      +2

                      Афинные типы реализуются в Rust (в runtime) в пол-пинка. Сделайте panic! в drop-трейте и в deref — и ваш тип афинный.


                      rank 2 в чистом виде — это большой WTF. Не знаю как для компилятора, но для программиста, читающего чужой код, точно. А ограниченный вид такого полиморфизма (в Rust) можно сделать через dyn-trait'ы, хотя я бы очень не хотел работать с кодом, который этим злоупотребляет.


                      Для отдельного класса случаев "ну мы же всё понимаем, это логгинг" отлично подходят макросы.

                        0

                        Уточнение: паника не требуется

                          0

                          А как вы обработаете нарушение требования афинного типа? Напомню, не линейного, а афинного — значение не только определяется, но и используется. Обязательно. Один раз. Если у вас значение не было использовано, то это уже не афинный, а линейный тип (то что в Расте). Так что придётся паниковать в дропе, чтобы показать, что значение не было использовано (если оно не было использовано).

                            0

                            Во-первых, всё наоборот: это значения линейного типа должны использоваться ровно 1 раз, а афинные можно забывать.


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

                              0

                              Тьфу, я всё попутал. Афинные типы в расте уже есть, а вот линейные надо подколхоживать.

                                0

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

                                0
                                Во-первых, всё наоборот: это значения линейного типа должны использоваться ровно 1 раз, а афинные можно забывать.

                                Так, тогда я уже запутался в терминологии. Я что-то думал, что аффинные типы — это когда мы вообще умеем накладывать ограничение на multiplicity, а линейные — их подмножество с multiplicity = 1.


                                Если это и правда не так, то пардон.


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

                                В pure FP трудно с автоматическим вызовом деструктора.

                                  0
                                  В pure FP трудно с автоматическим вызовом деструктора.

                                  Да, так и есть. Но Rust-то не pure FP, ему автоматически вызывать деструкторы можно!


                                  Есть ли какие-то применения линейных типов, которые не могут быть выражены в терминах деструктора?

                                    0

                                    Хороший вопрос. Смотря какая система типов навёрнута поверх деструкторов. В плюсах вот можно забыть их вызвать, если сделать new foo;.


                                    Возвращать данные из деструктора геморно. Работать с кодом, который симулирует пайплайны над линейными данными через деструкторы, мне бы не хотелось.


                                    Для отсутствия копирования надо уметь ещё и закрывать конструкторы копирования и операторы присваивания всякие.


                                    Линейными типами хорошо делать локальную мутабельность в иначе иммутабельных контекстах (не зря оно в Clean вместо монадок, и Clean даже с околонулевым инженерным эффортом в компиляторе по сравнению с ghc рвёт хаскель на некоторых задачах), и заодно накладывать дополнительные ограничения на семантику (map над списком с линейными типами не требует завтипов и индексации длины списка, корректная семантика и так получится, хотя строить теоремы на базе этого я бы тоже не хотел). Это, наверное, конечно, можно сделать через деструкторы… Но я даже не хочу думать, как.

                                      0
                                      Возвращать данные из деструктора геморно.

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


                                      Для отсутствия копирования надо уметь ещё и закрывать конструкторы копирования и операторы присваивания всякие.

                                      Мы же про Rust говорим? В Rust нет конструктора копирования, в Rust тип может быть либо тривиально копируемым, либо тривиально перемещаемым.


                                      Линейными типами хорошо делать локальную мутабельность в иначе иммутабельных контекстах

                                      Что такое "иначе иммутабельный контекст" в Rust? :-)

                            0
                            Афинные типы реализуются в Rust (в runtime) в пол-пинка. Сделайте panic! в drop-трейте и в deref — и ваш тип афинный.

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


                            rank 2 в чистом виде — это большой WTF. Не знаю как для компилятора, но для программиста, читающего чужой код, точно.

                            Не больше, чем borrow checker. Достаточно один раз понять этот паттерн с тегированными типами (а там на самом деле нет ничего сложного), и всё становится легко и просто и понятно.


                            Для отдельного класса случаев "ну мы же всё понимаем, это логгинг" отлично подходят макросы.

                            Тут моего Rust-fu точно не хватит. Насколько легко это комбинируется? Я могу иметь что-то вроде такого?


                            smthThatNeedsLogging :: MonadWriter [String] m => m ()
                            smthThatNeedsLogging = tell ["yay message"]
                            
                            smthThatNeedsEnvironment :: MonadReader Config m => m ()
                            smthThatNeedsEnvironment = ...
                            
                            combineBoth :: (MonadReader Config m, MonadWriter [String] m) => m ()
                            combineBoth = smthThatNeedsLogging >> smthThatNeedsEnvironment

                            Кстати, ещё я могу посмотреть на один тип smthThatNeedsEnvironment и сразу понять, что это бесполезная функция, но то такое.

                              0

                              Как меня поправили, в Rust афинные типы, а линейные надо самому делать (я их перепутал).


                              А вот если вы хотите пересказа на Rust хаскелевого кода, вам надо искать кого-то, кто по-хаскелевски говорит.

                                0

                                Ну, как вы на расте выразите что-то вроде «есть функция, которая требует толкьо доступ к эффекту логгирования, есть функция, которая требует доступ к эффекту ридонли-окружения, и есть функция, которая требует оба эффекта и вызывает те две безшовно»?


                                В хаскеле с mtl оно, кстати, начинает довольно быстро рваться, если строить нетривиальные стеки, где разные куски требуют разных контекстов, и начинаются всякие extensible effects, но это совсем другая история.

                                  0

                                  Сейчас я попробую понять что вы попросили. У нас есть функция, которой нужен доступ к логгеру (обычно он глобальный, но, допустим, это замыкание с сайд-эффектом), есть замыкание, которое использует переменные в R/O режиме. Вам интересно, как это сделать в Rust?


                                  В Rust есть такой примитив — RefCell, который позволяет реализовать interior mutability. Более того, принимающий замыкания (как аргумент функции) может указать какой тип замыкания ему нужен — FnOnce (можно вызвать только раз), Fn (вызов с замыканием без модификации) и FnMut (вызов с замыканием с модификацией). Примером такого может быть callback в GTK-rs, который требует, чтобы аргументы замыкания были немутабельными.


                                  RefCell позволяет передать немутабельный объект (сам refCell), который позволяет мутировать своё содержимое.


                                  Т.е. мы замыкаем не logger, а RefCell::new(logger::logger). RefCell позволяет сделать borrow_mut (занять значение для модификации), и в runtime проверяет, чтобы выполнялся инвариант.


                                  Наверное, если бы компилятор мог такое провернуть в compile time без дополнительного кода в runtime было бы круто.

                          +1
                          Так вроде же никто GIL не выпиливает? Просто в каждом субинтерпретаторе будет свой.
                          А объекты между субинтерпретаторами передавать через тот же Manager.
                          Накладных расходов не прибавится по сравнению с существующим multiprocessing.
                            0
                            На самом деле, для простого программиста на Python, GIL не решает ни каких проблем при разработке мультипоточных приложений. В статье не совсем корректно об этом «заявляется» (или косяк перевода).
                            GIL защищает не вашу программу на питоне, а только состояние самого интерпретатора. Вам же, как и обычно, надо использовать в мультипоточной программе разного вида локи, что бы бы избежать параллельного доступа к общим данным.
                            +1
                            Время запуска CPython даже без no-site составляет 100-200 мс (загляните на https://hackernoon.com/which-is-the-fastest-version-of-python-2ae7c61a6b2b, чтобы узнать больше).

                            Но в стать время запуска в зависимости от версии от 18 до 35 мс. И это не «даже без no-site», а просто «без no-site», потому что с no-site еще значительно быстрее:


                            $ time python3.7 -S -c ""
                            real    0m0.012s
                            
                            $ time python2.7 -S -c ""
                            real    0m0.007s

                            И вообще не понятно, при чем тут время запуска интерпретатора, если мультипроцессинг запускается форком.

                              +1

                              Подозреваю что 100-200 ms это в Windows, а 18-35 ms в Linux и других ОС.

                                0
                                А ещё это зависит от того HDD у вас или SSD
                                  +1

                                  Время запуска CPython — это ерунда. Первый же import ставит крест на любой exec-производительности. И чем дальше, тем хуже.

                                –4
                                вместо того, чтобы убрать GIL они придумывают костыли… печалька
                                  0
                                  А имеет ли смысл костылять бедный Python так, чтобы он ещё и в многопоточность умел? Мне кажется, для этого есть куда более подходящие инструменты. Таки зачем делать велосипед без очевидных преимуществ?
                                    +1
                                    Он уже умеет. Просто не очень удобно. Хотят сделать удобно. Зачем? Вы удивитесь но python мультипарадигменный язык практически универсального назначения. На нем например проводят анализ данных или математическую обработку, машинное обучение. Там много данных и хотелось бы это считать чуть быстрее малыми усилиями. А numpy и scipy оказались настолько мощными пакетами что породили большую группу подражателей на разных языках. Но там пока очень сырое все.
                                      0
                                      Нет, не удивили – я знаю, что такое Python :) Numpy и Scipy, если я ничего не перепутал, просто написаны на C. И все мощные фреймворки для перечисленных вами задач, типа TensorFlow, также написаны на C. Если решается реально ресурсоемкая задача, то все просто пишется на C. Для параллельного программирования есть куда более подходящие инструменты, я повторюсь. И взаимодействуют с C они не хуже, сохраняя при этом лаконичность. Выбор Python мне непонятен.
                                        +2
                                        Numpy и соответственно Scipy работают поверх библиотек OpenBLAS и LAPACK которые написаны преимущественно на Fortran.
                                        Ну и примеры лаконичных инструментов для «параллельного программирования» в студию )

                                          0
                                          Erlang/Elixir, например. В последнее время прямо балдею с них. Синтаксис не хуже питона, удобный тулинг, работают на акторах, библиотеки на C точно пишутся без проблем. Дальше можно тему не развивать? А то это понемногу превращается в ещё один бесполезный спор, мне кажется :)
                                    0
                                    Проще уж язык с нуля новый создать чем проблему GIL в Python решить.
                                    Тот же Dart после Python можно выучить за пару дней. Там и система типов удобная и синтаксис очень простой.
                                      0
                                      Речь не о языке, а об одном из его интерпретаторов. GIL — это не механизм Python, а механизм CPython.
                                      0

                                      Добавление в язык реальной многопоточности путём вытаскивания на свет интерпретатора, как сущности первого класса, с которой теперь можно реально работать и взаимодействовать — это отличное инженерное решение. Мне нравится.


                                      Смущает только, зачем называть их "interpreters"? Для рядовых программистов, если думать о будущем развитии и экспансии языка, гораздо понятнее было бы вывести эту сущность как "worker" (а ля ECMAScript) или нечто подобное.


                                      Ну, и вообще если подумать, раз уж был выставлен на всеобщее обозрение PyInterpreterState, почему бы не задуматься о добавлении возможности поманипулировать "снаружи" и его составными частями вроде PyFrameObject, с возможностью попереключать текущий контекст, стек и пр.
                                      Думаю, что если бы нечто подобное было реализовано с самого начала, то и никакая эпопея с asyncio не понадобилась. А gevent был бы простенькой pure-Python библиотечкой без сишных хаков...

                                        0

                                        То, что тут сделано, отличается от "старых" потоков лишь на вычислительных задачах, на задачах же ввода-вывода GIL ничуть не мешает. Причина существования asyncio — в другом: системный поток расходует слишком много ресурсов, а потому есть некоторый верхний предел числа соединений, выше которого многопоточный сервер уже не справляется.

                                          0
                                          Я почему то всегда думал что все дело в переключении контекста системой. Грубо говоря если количество потоков == количеству CPU в системе то все работает отлично. Если количество потоков >> CPU то начинают расти накладные расходы на переключение контекста.

                                          Кстати судя по рейтингам asyncio выступил просто отлично.

                                          104 starlette 378,262
                                          186 aiohttp 143,872
                                          195 django-py3 129,911
                                          228 flask 68,481
                                            0

                                            Ну да, так и есть. Однако, вырастают эти накладные расходы до заметного уровня все же не сразу — отсюда и появляются идеи насчет того, что многопоточные сервера вроде как работают нормально, а asyncio пришлось добавлять в язык из-за GIL...

                                              0
                                              отсюда и появляются идеи насчет того… asyncio пришлось добавлять в язык из-за GIL

                                              Мой комментарий был совсем не об этом. Видимо я недостаточно распространённо высказался из-за чего могло так показаться.


                                              Контест следующий:


                                              1. Изначально, ещё до всякого появления asyncio в экосистеме питона уже была (и есть) приличная и удобная возможность запускать "зелёные потоки": gevent.
                                              2. Раздавались предложения её стандартизовать, но были отвергнуты, видимо потому, что там переключение происходит слишком неявно, а это не "Python Way".
                                              3. Вместо этого Гвидо, никого не спрашивая решил, что сможет нас всех осчастливить реализацией некоего аналога .NET-овского async/await вообще без каких-либо дополнительных изменений в ядре интепретатора. Вся работа строилась на основе только что появившейся в Python 3.3 конструкции yield from.
                                              4. Так в Python 3.4 появился asyncio (PEP 3156). Многие были не очень-то довольны волюнтаризмом Гвидо при продвижении (почти без обсуждения) столь значимой библиотеки в язык. Но раз уж это была просто ещё один пакетик в python/lib, поворчали и смирились — одним больше, одним меньше.
                                              5. Уже после этого кавалерийского релиза выяснилось, что не всё так просто и без серьёзных дополнений в ядро интерпретатора asyncio практически бесполезен.
                                                А раз уж он теперь часть стандартной библиотеки — не выбрасывать же. Коготок увяз — всей птичке хана :) Так в Python 3.5 пришлось срочно имплементировать настоящие корутины (см. PEP 432).

                                              И вот я думаю. А что если бы этот ныне открываемый PyInterpreterState (а особенно его более мелкие части вроде PyFrameObject) были с самого начала доступны нам для базового манипулирования? Не вышло бы тогда, что и вся вышеперечисленная неприятная эпопея не понадобилась бы? А все желащние реализовывать зелёные потоки могли бы делать это на чистом Питоне и не париться.
                                              Никакого особо развитых API для этого не нужно ведь, просто возможность сохранить указатель на текущий (хранящийся в куче) стек питоновских вызовов, передать/сохранить её куда нужно и переключить поток управления между этими фреймами на время ожидания сигнала.

                                                0

                                                Насколько я вижу, в текущем виде PyInterpreterState в принципе не может предоставить для изменения свои "более мелкие части вроде PyFrameObject" по той простой причине, что PyFrameObject являются частью потока, а не интерпретатора.


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


                                                Не уверен что это все в принципе может оказаться быстрее чем нормальные потоки, не говоря уже о нормальных (т.е. языковых) сопрограммах.

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

                                      Самое читаемое