Pull to refresh

Comments 48

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

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

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

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

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

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


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

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

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


a = foo(a, ...)

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


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


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


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


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


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

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

UFO just landed and posted this here

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


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


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

UFO just landed and posted this here
UFO just landed and posted this here

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


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


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

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

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

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


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

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

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

UFO just landed and posted this here
В pure FP трудно с автоматическим вызовом деструктора.

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


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

UFO just landed and posted this here
Возвращать данные из деструктора геморно.

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


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

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


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

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

UFO just landed and posted this here

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


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

UFO just landed and posted this here

Сейчас я попробую понять что вы попросили. У нас есть функция, которой нужен доступ к логгеру (обычно он глобальный, но, допустим, это замыкание с сайд-эффектом), есть замыкание, которое использует переменные в 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 было бы круто.

Так вроде же никто GIL не выпиливает? Просто в каждом субинтерпретаторе будет свой.
А объекты между субинтерпретаторами передавать через тот же Manager.
Накладных расходов не прибавится по сравнению с существующим multiprocessing.
На самом деле, для простого программиста на Python, GIL не решает ни каких проблем при разработке мультипоточных приложений. В статье не совсем корректно об этом «заявляется» (или косяк перевода).
GIL защищает не вашу программу на питоне, а только состояние самого интерпретатора. Вам же, как и обычно, надо использовать в мультипоточной программе разного вида локи, что бы бы избежать параллельного доступа к общим данным.
Время запуска 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

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

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

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

вместо того, чтобы убрать GIL они придумывают костыли… печалька
UFO just landed and posted this here
Он уже умеет. Просто не очень удобно. Хотят сделать удобно. Зачем? Вы удивитесь но python мультипарадигменный язык практически универсального назначения. На нем например проводят анализ данных или математическую обработку, машинное обучение. Там много данных и хотелось бы это считать чуть быстрее малыми усилиями. А numpy и scipy оказались настолько мощными пакетами что породили большую группу подражателей на разных языках. Но там пока очень сырое все.
UFO just landed and posted this here
Numpy и соответственно Scipy работают поверх библиотек OpenBLAS и LAPACK которые написаны преимущественно на Fortran.
Ну и примеры лаконичных инструментов для «параллельного программирования» в студию )

UFO just landed and posted this here
Проще уж язык с нуля новый создать чем проблему GIL в Python решить.
Тот же Dart после Python можно выучить за пару дней. Там и система типов удобная и синтаксис очень простой.
Речь не о языке, а об одном из его интерпретаторов. GIL — это не механизм Python, а механизм CPython.

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


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


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

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

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

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

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

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

отсюда и появляются идеи насчет того… 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 для этого не нужно ведь, просто возможность сохранить указатель на текущий (хранящийся в куче) стек питоновских вызовов, передать/сохранить её куда нужно и переключить поток управления между этими фреймами на время ожидания сигнала.

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


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


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

Sign up to leave a comment.