Комментарии 27
Тут наверное можно долго спорить для чего подходит Python.
На нем можно решать CPU-intensive задачи, да не самим питоном, но внешними библиотеками (например, numba, numpy, scipy, pandas, py-arrow и т.д.) либо разрабатывать свои собственные расширения (на C, C++, Rust) и такого рода задачи вполне можно пускать в отдельных потоках, управляя ими из приложения, написанного на Python.
К тому же, asyncio появился только в 3.4, а есть проекты, которые ведут свою историю раньше, и в которых такого рода задачи надо решать.
Вы очень категоричны в своих суждениях и, тем более, в развешивании ярлыков. Подход о котором вы пишите известен всем, кто с данной проблемой сталкивается.
Вам же я процитирую PEP703, который вы не читали, как и код, на который даны ссылки:
The Steering Council accepts PEP 703, but with clear proviso: that the rollout be gradual and break as little as possible, and that we can roll back any changes that turn out to be too disruptive – which includes potentially rolling back all of PEP 703 entirely if necessary (however unlikely or undesirable we expect that to be).
Я вам ваше, в своем первом комментарии, написал, какие есть варианты написания многопоточного кода на Python, который мог бы заниматься осмысленной работой и при этом GIL ему бы не мешал. У меня складывается мнение, что вы не понимаете как работает GIL в Python и зачем он нужен.
Допустим у нас есть несколько больших массивов numpy, с которыми мы должны сделать какую то операцию и получить третий массив. Здесь код, который эту операцию выполняет вполне может быть запущен как в один поток, так и в несколько. Или другой пример, архивация данных - тоже может быть выполнена в несколько потоков, даже с применением OpenMP. При всем при этом вы можете отпустить GIL и другие потоки, например, главный поток приложения будет продолжать взаимодействовать с пользователем.
Если вы готовы это конструктивно обсуждать (в чем я не уверен), мы можем даже посмотреть какие-то примеры кода.
Возвращаясь к вашему первому примеру, вы там сделали правильную ремарку То есть на C
с атомарными переменными - т.е. вы вполне понимаете, что даже на С++ код `x = x + 1` выполнится атомарно, только если x имеет тип, например, std::atomic<int32_t>. Тогда почему вы отказываете питону возможность реализовать ту же операцию с использованием таких же "атомиков" под капотом?
В том же питоне, GIL вам не запрещает работать с одним и тем же объектом из разных потоков (например, пополнять список). GIL дает вам гарантию, что внутреннее состояния списка не будет "разрушено" в ходе работы двух потоков. В том же PEP703 помимо всего прочего предлагается реализовать объектные блокировки.
В целом там много интересного, заставлять его прочитать вас я не могу, да и не хочу.
Во-первых, я не говорил о списках на атомиках, я написал про объектные блокировки, это несколько иное. Во-вторых, lock-free структуру данных известны давно (даже здесь на хабре есть статье про них Lock-free структуры данных. Iterable list / Хабр), это к разговору о том, что есть структуры данных, безопасные для использования в многопоточном окружении, но которые не используют блокировки.
Относительно переменных, вы правы, что просто так атомиками там не получится сделать. Но проблема там не в том, что переменная это структура данных, а в том, что Python поддерживает длинную арифметику - вот пример того как сделано сложение - cpython/Objects/longobject.c at eac41c5ddfadf52fbd84ee898ad56aedd5d90a41 · python/cpython. И есть перевод разбора, как эта арифметика работает - Как в Python реализованы очень длинные числа типа integer? / Хабр.
Думаю, что в рамках отказа от GIL тут тоже придется, что то придумывать.
Вы задаете довольно очевидные вопросы и сами даете на них довольно очевидные ответы. Еще раз — все кто хоть раз серьезно сталкивался с многопоточным программированием об этом знают.
Процесс работы открыт — сформулирован спектр проблем, сформулированы возможные решения, код пишется и коммитится (в открытом репозитории) — вполне можно к этому источнику знаний припасть и помочь ценными замечаниями.
С другой стороны, перетирать труизмы в комментариях на хабре гораздо проще.
Прошу прощения, не знаком с питоном глубоко, но разве эта вот проверка
if (0 == self->count && 0 == self->pending_count){
не может произойтиодновременно несколькими потоками, что если один поток не успеет выполнить код блока ниже, ы то время как другой уже завершит эти же
и еще : попробуйте протестировать на 100000 потоках, 10 что то слабовато.
И код тестирования не проведён или я невнимателен
Код для тестирования в репозитории у автора fastrlock
- fastrlock/lockbench.py at master · scoder/fastrlock, я туда добавил только h5py (что не заслуживает какого-то отдельного выкладывания).
Ссылку добавлю в статью, спасибо!
Этот код защищен GIL, поэтому исполняется только одним потоком в единицу времени. Пока мы не освободим GIL (через вызов Py_BEGIN_ALLOW_THREADS) другой поток в нашу функцию fastrlock_acquire попасть не может.
На 100_000 потоках тестировать это смысла нет (как впрочем и запускать такое количество потоков, только если они не занимаются ожиданием на IO).
Да даже если они занимаются IO - 100к обычных тредов скорее всего поставят на колени шедулер ОС.
получается вся магия закрыта в волшебной директиве Py_BEGIN_ALLOW_THREADS
А как она работает ? в том смысле, не кроется ли за ней обычный медленный системный мьютекс ? Вот потому и попросил вас протестовать свое решение в условиях работы одновременно 100к потоков. продумайте тестовый код, который создаст реальную конкуренцию 100к потоков на одном защищаемом Вами ресурсе. тогда и будут видны преимущества приведенного в статье решения. и может ускорение уже не будет таким очевидным из-за Py_BEGIN_ALLOW_THREADS.
но последнее- предположение.
Отвечу на оба два ваших комментария здесь.
Есть различные задачи, под которые пишется код. Есть задачи IO-intensive, есть CPU-intensive. В первом случае, у вас потоки много ждут на операциях ввода-вывода и большую часть времени не занимают ресурсы процессора (но при этом все равно продолжают потреблять оперативную память). Как правильно ядро ОС будет реже на них переключаться, но это все равно будет занимать процессорное время. Во втором случае, потоки занимаются "число дробительными" задачами. Но т.к. у нас вытесняющая многозадачность, то это значит что ядро ОС все равно эти потоки будет прерывать и отдавать квант времени другим потокам (с учетом приоритетов и всего прочего). В этом случае нам не нужно использовать больше потоков, чем у нас есть ядер в процессоре. Тут можно так же упомянуть Закон Амдала — Википедия (простите за ссылку на википедию).
В случае 100к потоков, у нас шедулер ОС будет заниматься только тем что будет между потоками переключаться - т.е. весь ресурс процессора будет отдан только на переключение между потоками.
По второму вопросу - Py_BEGIN_ALLOW_THREADS это просто макрос, который сохраняет текущее состояние потока и вызывает PyEval_SaveThread, который освобождает GIL. Как правило GIL это обычный системный мьютекс. Но если внимательно читать статью, то я писал, что в любом случае мы этим мьютексом защищены - вызываем мы fastrlock
или RLock
. И преимущество мы достигаем, за счет самой реализации реентерабельной блокировки.
И возвращаясь к 100к потокам и RLock - никто в здравом уме не будет запускать 100к (даже если бы имел смысл) и ходить ими в защищаемый ресурс. На таких числах работают уже другие подходы, например, data parallelism.
Заголовок кликбейт. Реализация у вас немного не на Питоне.
Стандартная реализация может использовать для блокировки мьютекс или семафор, и их захват всегда приводит к вызову функции из ядра ОС
Выделенное не является верным. Очень странно видеть столь общие и категоричные утверждения. Во многих UNIX-ах эти детские болезни давно пролечены. Как минимум, в первую очередь производится попытка захвата через атомик cmp&swap (CAS):
Пример из glibc: https://github.com/lattera/glibc/blob/master/nptl/pthread_mutex_lock.c#L201
Да и само название базового примитива futex (Fast Userspace muTEX) тоже намекает на обратную картину. Есть примеры и из других ОС.
Спасибо большое за хороший вопрос. Я, к моему сожалению, не знаю как в Linux посмотреть, какой тип мьютекса используется. Я вижу, что вызовы уходят в pthread_mutex_trylock
и pthread_mutex_lock
, но какая там реализация — не знаю. Если вы подскажете, буду очень благодарен.
Я добавил результаты тестирования для Fedora 39, Ubuntu 20.04, 24.10 — если вам интересно, посмотрите в таблице. Также я переформулировал первый абзац.
Как реализовать быструю реентерабельную блокировку на Python и почему она работает