47

Закон дырявых абстракций и GIL в python

Давным давно Джоел Спольски рассказал о "законе дырявых абстракций" (оригинал от 2002 года). В современном мире, чтобы починить проблему, часто надо уметь работать на уровень ниже текущего уровня абстракции. Проблема может быть с compose, docker, конкретной библиотекой, python, операционной системой, сетью, железом... Чем больше абстракций вы знаете, тем больше вероятность, что вы сможете решить проблему следующего уровня.


Нельзя в один момент освоить десяток нужных инструментов и абстракций. Нужно плавно расширять используемый инструментарий. Освоили git? Ни строчки кода далее без него. Научились тестам? В каждом проекте их нужно писать с самого начала. Теперь Docker в копилке? Применяем, если это уместно. Чем больше опыта в разных технологиях, тем вы сильнее как специалист.


Расскажу о своём опыте. В статье как расширить технический кругозор я делился, что для ориентирования в технологиях я постоянно читал хабр (2010-2015 года, самый расцвет технического контента там). Пришёл ко мне коллега со следующим вопросом. Я, говорит, выгружаю строю граф друзей в социальной сети, для этого массово скриптом на python выгружаю оттуда списки всех друзей и складываю в mongodb. Запускаю выборку на N человек на 1 потоке — скрипт работает 60 секунд. Запускаю на 10 потоках — скрипт работает 70 секунд. Мне надо N увеличить и запуститься на сутки, но какого чёрта увеличение числа процессоров замедляет выполнение?  Где проблему искать?

Это питон тормозит?

В монге проблемы?

С компом что-то не так?

Социальная сеть меня банит?

Сеть тормозит?

Где вообще искать беду?


А я просто знал ответ. Прочитал накануне статью про GIL в python. На вики она выглядит так. Если кратко, то из-за потоковой небезопасности кода на Си, который внутри всех стандартных библиотек питона, интерпретатор физически работает на одном ядре, а многопоточность реализована с блокировками. Это не важно для IO-bound задач (когда код ждёт внешних данных), но критична для CPU-bound задач (когда реально надо все ядра использовать).

Для починки всего-то и надо, что заменить модуль многопоточности threading.Thread на многопроцессность multiprocessing.Process. Теперь работают 10 независимых процессов, которые делают своё черное дело. У них нет связи (общего адресного пространства), которое есть у потоков. Но в этой задаче связь и не нужна была, процессу выдавался пул адресов для анализа.

И теперь 60 секунд на 1 ядре превратилось в 10 секунд на 10 ядрах. Да, не в 10 раз ускорилось, но это вполне годное ускорение. А ещё можно посмотреть, как делают рядом и воспользоваться топовым инструментом.


В телеграм-канале разбираем разные нюансы из жизни разработчика на Python и не только — python, bash, linux, тесты, командную разработку.

Правила сообщества

Публиковать могут пользователи с любым рейтингом. Однако!


Приветствуется:

• уважение к читателям и авторам

• конструктивность комментариев

• простота и информативность повествования

• тег python2 или python3, если актуально

• код публиковать в виде цитаты, либо ссылкой на специализированный сайт


Не рекомендуется:

• допускать оскорбления и провокации

• распространять вредоносное ПО

• просить решить вашу полноценную задачу за вас

• нарушать правила Пикабу

1
Автор поста оценил этот комментарий

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

раскрыть ветку (1)
1
Автор поста оценил этот комментарий

Такой вот костыль в питоне, о котором стоит знать :) Типа GIL - вынужденный хак, который повышает производительность однопоточного кода, но создаёт проблемы в многопоточном. Так как большинство скриптов реально однопоточные (или хотя бы IO-bound), то быстрая работа однопоточного кода считается более приоритетной.


Есть реализации интерпретатора питона без GIL, типа Jython или IronPython.

1
Автор поста оценил этот комментарий

Так, а чем поможет asyncio в CPU bound задачах?
Да ладно, фреймворки только зарождались, а как же twisted, tornado - забытые технологии древних?
Или просто не модные :)

раскрыть ветку (1)
1
Автор поста оценил этот комментарий

Как раз думал о tornado и twisted. Но они оба мимо меня прошли, только слышал их как "не продакш решения" тех лет.


Не говорю, что asyncio поможет. Хотя можно посмотреть на ProcessPoolExecutor, например


Тут даже вопрос, что именно коллега делал. Потому что параллельный скрапинг соц-сети не должно быть CPU-bound, но как раз IO-bound. Но раз фикс помог, что он где-то в CPU и GIL утыкался

1
Автор поста оценил этот комментарий
Спасибо, меня в этом направлении taichi привлёк, но там только численные или векторные вычисления, зато одной строкой можно кинуть либо на cpu или gpu, даже Cuda работает (я так понимаю принцип граф там реализован, насколько я вообще могу это понять)...чем-то схож по Применению с модулем numba, однако скорости бешеные, не побоюсь сказать, приближенных к си подобным языкам. Где то даже видос есть с построение фракталов сравнительный (с использованием разных модулей), я вначале не поверил, решил у себя потестить, оказалось....пушка.
раскрыть ветку (1)
1
Автор поста оценил этот комментарий

Хороший опыт, спасибо

1
Автор поста оценил этот комментарий

а почему subprocess вместо multiprocessing? и в чем разница?

Лично я для буфера загрузчика текстур в играх использую threading (заметно увеличить фпс можно, так как ожидание сигнала от носителя есть, я думаю с бд даже бы история была), а вот для вычислений\операций обычно к multiprocessing прибегаю, но без злоупотребления, и там и там через run()

P.s. я любитель, но часто приходиться с распараллеливанием на процессы и потоки работать.

раскрыть ветку (1)
1
Автор поста оценил этот комментарий

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


Если нужно общее адресное пространство, то threading не избежать. Для multiprocessing приходится организовывать передачу данных каким-либо образом, потому что общей памяти нет.

показать ответы
1
Автор поста оценил этот комментарий

А почему subprocess, а не multiprocessing?

раскрыть ветку (1)
1
Автор поста оценил этот комментарий

Хм, кажется это был multiprocessing.Process, верное замечания. Попутал, т.к. недавно subprocess на sh переделывал в pet-проекте

1
Автор поста оценил этот комментарий

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

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

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

Например, в https://t.me/devfm/101 рассматривалась статья, где для разных брокеров сообщений измерялись сетевые задержки. В случае kafka по сети сетевая задержка может быть порядка 20 мс и пропускной способностью в сотни mb/s. Вот тут сравнивают варианты передачи данных по пропускной способности https://www.baeldung.com/linux/ipc-performance-comparison , порядок токе сотни mb/s.

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

1
Автор поста оценил этот комментарий

Скрапи использовать - не по мужски ))

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Ну, я даже задачу его точно не знаю, и не знаю что он внутри делал. Возможно, он через scrapy и делал. Но это не снимает вопроса GIL. По крайней мере, на stackoverflow пишут


The recommended way for working with scrapy is to NOT use multiprocessing inside the running spiders. The better alternative would be to invoke several scrapy jobs with the respective separated inputs.


А это приводит опять к вопросу многопоточности или многопроцессности

3
Автор поста оценил этот комментарий

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

В python надо знать про gil.

В современном питоне есть ещё асинхронное выполнение, оно тоже бывает полезно.

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Тогда asyncio только-только зарождался и были только экспериментальные веб-фреймворки. Теперь то живём)

показать ответы
1
Автор поста оценил этот комментарий

А где лежали данные графа? В соц сети или где-то локально?

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

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