LINUX.ORG.RU

Разделяемая память для процессов на Python и не только.

 , , , ,


2

4

Привет всем!

Есть большое многопроцессное приложение, с рабочими процессами, всякими GUI и логгерами в отдельных процессах и т.д. Построено с использованием модуля multiprocessing. Рабочие процессы обрабатывают большие данные. Для больших данных используется multiprocessing.Array, вот так:

class Worker(mp.Process):
    def __init__(self, buffers, pipe, other):
        super().__init__()
        self._buffers = buffers
        self._pipe = pipe
    def run(self):
        param = get_message(self._pipe)
        big_calculations(self._buffers[param.a], param.b, ...)

buffers = []
for i in range(10):
    buffers.append(mp.Array(ctypes.c_uint8, buffer_size, lock=False))
...
for i in range(10):
    p1, p2 = mp.Pipe()
    worker = Worker(buffers, p1, other_param)
    worker.start()
Синхронизация от mp.Array не требуется, процессы синхронизируются с помощью посылки/отправки сообщений через p1/p2, поэтому lock=False.

Вопросы:
* Размер/количество буфера(ов) задаются до запуска рабочих процессов. Как правильно реализовать изменение количества/размера после того, как рабочие процессы уже стартовали? Т.е. понятно, как отправить сообщение. Непонятно как закрыть существующий буфер и открыть новый.
* Что у mp.Array под капотом? Я заметил, что python открывает много файлов с именами вида /tmp/pymp-ixc54qx7/pym-27111-h7wi_sy3. Это как-то связано с mp.Array?
* Очень желательно, чтобы к этой общей памяти можно было обращаться не только из процессов на Python. Возможно как-либо её открыть из другого стороннего процесса, написанного на чём-то ещё? Может быть мне тогда что-то другое использовать, а не mp.Array?

Аналогичные вопросы про mp.Pipe():
* Как в работающий процесс передать конец новой трубы?
* Как передать в не Python'овский процесс конец трубы?

Спасибо!

★★★★★

Последнее исправление: ls-h (всего исправлений: 1)

Возможно как-либо её открыть из другого стороннего процесса, написанного на чём-то ещё?

RabbitMQ. Воркеров можно запускать на даже разных машинах и распределять задачи. Например, тяжелые обработки делать на с++ или go.

Uncle_Bobby
()
Ответ на: комментарий от Uncle_Bobby

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

Пока такой задачи нет. Те большие данные, о которых речь, обрабатывать надо на одной машине, максимально быстро.

ls-h ★★★★★
() автор топика

multiprocessing внутри использует примитивы операционной системы. Ответы на твои «можно ли» и «как» зависят от ОС. Решения будут для конкретной ОС. Для разных ОС придётся писать разные решения.

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

anonymous
()

Я пришёл к выводу что multiprocessing непригоден для написания реального кода и лучше держаться от него подальше. fork + pipe/unix socket и все вопросы взаимодействия с не-питоном и передачи в другие процессы отпадают. Через pipe/socket можно передавать как бинарные данные так и pickle’нутые питоновые структуры. Через unix socket также можно передавать файловые дескрипторы, но я считаю это хаком и лучше передавать пути в файловой системе.

slovazap ★★★★★
()
Ответ на: комментарий от slovazap

Тоже пришёл к такому выводу. Этот multiprocessing глючен-крив, как вся моя жизнь. Начиная с того, что даже не умеет по Ctrl-C корректно завершать запущенные процессы. И это только вершина навозной кучи. То таинственные дедлоки в продакшоне по чётным числам в пятницу, то процессы молча мрут, а ошибки не обрабатываются. Типичная протекающая абстракция.

anonymous
()
Ответ на: комментарий от slovazap

Я пришёл к выводу что multiprocessing непригоден для написания реального кода

Почему непригоден? И, если дочерние процессы создавать вручную, то не возникнет ли проблем с отладкой? Сейчас, используя PyCharm, я могу ставить breakpoint'ы в разных процессах и отладчик их ловит. Если отказаться от mp, как отлаживать?

Через pipe/socket можно передавать как бинарные данные так и pickle’нутые питоновые структуры

На данный момент mp.Pipe удобен тем, что сам делает и pickle/unpickle и чтение из такой «трубы» просто возвращает объект. При ручной работе с pipe/socket надо будет самому велосепедировать pickle/unpickle или есть что-то готовое?

но я считаю это хаком

А что в этом плохого?

ls-h ★★★★★
() автор топика
Последнее исправление: ls-h (всего исправлений: 1)
Ответ на: комментарий от anonymous

Этот multiprocessing глючен-крив

И чем его заменить? Просто запускать отдельные процессы и общаться сними через pipe/socket?

ls-h ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Ответы на твои «можно ли» и «как» зависят от ОС

Linux, конечно. Это же не винфак.

что-то готовое, что умеет разные языки программирования

Что?

и ОС

Такой задачи нет.

ls-h ★★★★★
() автор топика

mp.Array внутри использует разделяемую память, точнее говоря файл отображенные в память в shared-режиме. Это работает примерно одинаково во всех операционках (с поправкой на то что windows не позволяет штатно увеличивать размер на ходу, и совсем не позволяет го уменьшать).

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

Если обрабатываемые данные можно представить в виде key-value (где value может быть до гигабайта размером), то возможно подойдет libmdbx. Грубо говоря, это шустрый key-value в разделяемой памяти с ACID. Но пока у меня нет байдингов для питона (приму pull-request и т.п.).

Deleted
()
Ответ на: комментарий от ls-h

В смысле? Он один, доступен через api ядра. Тебе ведь надо общий доступ из разных процессов, например на java или C++? Ищи биндинги на питоне к mmap.

Uncle_Bobby
()
Ответ на: комментарий от Uncle_Bobby

Он один, доступен через api ядра

Мне почему-то казалось, что есть несколько вариантов, как минимум sysv и posix.

ls-h ★★★★★
() автор топика
Ответ на: комментарий от ls-h

Почему непригоден?

Я уже не вспомню конкретику. У меня была модель мастер→процессы-обработчики→обратно в мастер, и я намучился просто феерически, потому что примитивы mp не умели базовые вещи для этого необходимые. То прерывание работы нормально не обработать, то конец очереди не отловить, то не заблокироваться когда очередь переполнена, то не уничтожить нормально непустую очередь из которой никто уже не читает, то дедлок в мастере, который очень сложно обходится, что-то из этого класса проблем, разнообразное и обильное.

И, если дочерние процессы создавать вручную, то не возникнет ли проблем с отладкой?

Я не пользуюсь отладкой, так что не знаю. Попробуйте, там нет никакой магии - mp на том же форке и пайпах и построен.

На данный момент mp.Pipe удобен тем, что сам делает и pickle/unpickle и чтение из такой «трубы» просто возвращает объект. При ручной работе с pipe/socket надо будет самому велосепедировать pickle/unpickle или есть что-то готовое?

Вам же нужно взаимодействие с не-питон процессами, какой там пиклинг? А так, это либо обёртка на 10 строчек, или добавление pickle()/unpickle() в write/read. Ну либо конкретно здесь и mp.Pipe сгодится. Без остального mp.

А что в этом плохого?

Как минимум, непереносимо в целом и привязывает к сокетам не unix, когда вполне хватило бы пайпов. Есть подводные камни, что-то там в man было про передачу закрытого дескриптора. Для высокоуровневых языков нужны дополнительные телодвижения для развёртывания высокоуровневого примитива для вытаскивания дескриптора на одной стороне и обратного процесса на другой. В конце концов, файловые дескрипторы это приватный ресурс процесса, и любые попытки их шаринга и передачи - нарушение инкапсуляции, усложнение архитектуры и сад граблей.

slovazap ★★★★★
()
Последнее исправление: slovazap (всего исправлений: 1)
Ответ на: комментарий от ls-h

Linux, конечно. Это же не винфак.

Это opnesource.ru. Кто тебя знает, может ты под иллюмосом запускать будешь.

Что?

Да кто ж тебя знает-то?

Для «больших данных» всякие хадупы используют, но ты до них явно не дорос если тебе multiprocessing хватало.

Используй mmap с MAP_SHARED и не парь мозг.

anonymous
()
Ответ на: комментарий от Uncle_Bobby

Ищи биндинги на питоне к mmap

Есть модуль, так и называется mmap. Единственно что непонятно, это как сделать mmap без записи файла и взаимодействия с ФС. mmap (из модуля mmap) первым параметром принимает файловый дескриптор. Дескриптор может быть -1, тогда файл не создаётся, но и имени у него не будет. Как тогда поделиться этим участком памяти с другими процессами? В POSIX же есть shm_open, которая возвращает дескриптор по имени (как обычный open), но, насколько я понимаю, запись на диск при этом не производится.

ls-h ★★★★★
() автор топика

Для больших данных надо Go/C использовать. Python и многопоточность - это тупо костыли, и так... «чтобы было», не далеко от Си ушло, несмотря на то, что, казалось бы, Питон. Я бы из Python ставил задачу в очередь, и обрабатывал бы на Go или Сишке. Первое предпочтительное. Но на сишке можно нативный python module написать :)

Так что Python - RQ/Nats/Celery/ZeroMQ - Go engine

https://github.com/rq/rq

http://www.celeryproject.org/

https://nats-io.github.io/docs/

menangen ★★★★★
()
Последнее исправление: menangen (всего исправлений: 1)
Ответ на: комментарий от ls-h

Дескриптор может быть -1, тогда файл не создаётся, но и имени у него не будет. Как тогда поделиться этим участком памяти с другими процессами?

Только через fork.

В POSIX же есть shm_open, которая возвращает дескриптор по имени (как обычный open), но, насколько я понимаю, запись на диск при этом не производится.

Да, но… В зависимости от реализации, но обычно создается файл где-нибудь в /tmp или /dev/shmю

На Linux можно смело использовать /dev/shm, т.е. создать там файл и юзать дескриптор для mmap.

Deleted
()
Ответ на: комментарий от Deleted

Только через fork.

Это не подходит. Что делать, когда нужно создать новый буфер после запуска процессов рабочих?

На Linux можно смело использовать /dev/shm

Тоже такая мысль пришла, но что-то ничего не нагуглил, как корректно это делать. Можно выбрать любое имя? Нужно вручную очищать перед завершением последнего процесса или как только не будет ни одного процесса, память будет освобождена?

ls-h ★★★★★
() автор топика
Ответ на: комментарий от ls-h

я по питону не специалист, но в доке на модуль mmap есть параметр tagname.

https://docs.python.org/3/library/mmap.html

tagname, if specified and not None, is a string giving a tag name for the mapping. Windows allows you to have many different mappings against the same file. If you specify the name of an existing tag, that tag is opened, otherwise a new tag of this name is created. If this parameter is omitted or None, the mapping is created without a name. Avoiding the use of the tag parameter will assist in keeping your code portable between Unix and Windows.

Как то мутно это написано.

Uncle_Bobby
()
Ответ на: комментарий от ls-h

Тоже такая мысль пришла, но что-то ничего не нагуглил, как корректно это делать. Можно выбрать любое имя?

mkstemp в /tmp

Нужно вручную очищать перед завершением последнего процесса или как только не будет ни одного процесса, память будет освобождена?

Нужно unlink, можно сразу после того, как все процессы открыли файл.

anonymous
()
Ответ на: комментарий от Uncle_Bobby

Как то мутно это написано.

Ага, я тоже не понял. Это какое-то вендоспецифическое. И в (Unix version) ничего такого не предлагается.

ls-h ★★★★★
() автор топика
Ответ на: комментарий от ls-h

Это не подходит. Что делать, когда нужно создать новый буфер после запуска процессов рабочих?

Если без fork() от основного процесса, то только через создание файлов. В конечном счете shm_open() делает ровно тоже самое.

Можно выбрать любое имя?

Ну имя должно быть либо уникальное и заведомо известно, либо http://man7.org/linux/man-pages/man3/mkstemp.3.html. Но при уникальном авто-генерируемом имени его нужно как-то сообщить каждому процессу в «оркестре».

Нужно вручную очищать перед завершением последнего процесса или как только не будет ни одного процесса, память будет освобождена?

В /dev/shm на актуальных linux-ах смонтирована tmpfs, и всё что там лежит будет «жить» в ОЗУ пока к inode есть ссылки:

  1. когда файл есть в какой-то директории.
  2. пока есть хотя-бы один процесс, в котором этот файл открыт или отображен в память.

Т.е. файл нужно удалить когда он станет ненужным, иначе будет просто занимать ОЗУ до перезагрузки. При этом можно удалить как только все процессы из «оркестра» открыли файл и/или отобразили в память.

Однако, как только файл будет удален «просто так» к его содержимому уже не подключишься (только открывая соответствующий дескриптор в /proc/$PID/fd/

Deleted
()
Ответ на: комментарий от Deleted

Дескриптор может быть -1, тогда файл не создаётся, но и имени у него не будет. Как тогда поделиться этим участком памяти с другими процессами?

Сделать дескриптор через memfd_create, делиться с другими процессами через /proc/<pid>/fd/<fd>.

anonymous
()
Ответ на: комментарий от ls-h

Ага, я тоже не понял. Это какое-то вендоспецифическое. И в (Unix version) ничего такого не предлагается.

В linux это реализуется созданием файла вида /dev/shm/LIKE_WINDOWS_NAMED_SHARED_MEMORY_$NAME.

Внутри Windows это реализовано примерно также, но имена этих файлов видны через Native API, а не всем. Важное отличие в том, что Windows самостоятельно удаляет такой файл с закрытием последнего дескриптора.

В linux же такое «авто-удаление» нужно реализовывать отслеживая закрытие дескрипторов, в том числе начиная танцевать с O_TMPFILE-бубном.

Deleted
()
Ответ на: комментарий от anonymous

Сделать дескриптор через memfd_create, делиться с другими процессами через /proc//fd/.

Кстати да, про относительно новый memfd_create() я забыл. Удобство в том, что имя явно видно в /proc/$PID/fd/

Но только это почти тоже самое, как если начать с fork().

Deleted
()
Ответ на: комментарий от Deleted

Но только это почти тоже самое, как если начать с fork().

Да ну нет. Созданный через memfd_create файл ты можешь открыть в паралельном процессе, о чем явно сказано в манах.

А профит ещё в том, что не надо париться с уникальным именем и руками освобождать память.

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

anonymous
()
Ответ на: комментарий от anonymous

Да ну нет. Созданный через memfd_create файл ты можешь открыть в паралельном процессе, о чем явно сказано в манах.

А профит ещё в том, что не надо париться с уникальным именем и руками освобождать память.

Ок, уговорил что лучше.

Но я имел в виду, что запуск нужно начинать с какого-то первого/главного процесса. А тогда можно сделать еще так = в этом процессе создать файлик c O_TMPFILE, сдвинуть его дескриптор посредством dup2() на какой-нибудь 142й номер, и потом открыть во всех потомках.

Deleted
()
Ответ на: комментарий от Uncle_Bobby

Вот примеры для питона

Спасибо!

ls-h ★★★★★
() автор топика
Ответ на: комментарий от Deleted

O_TMPFILE

У O_TMPFILE есть особенность, что файл создаётся в файловой системе в указаной в open директории. Если нужно, чтобы файл лежал в оперативной памяти, то надо найти и указать в open директорию, лежащаю в tmpfs. Иначе будет писать на диск.

anonymous
()
Ответ на: комментарий от Deleted

только открывая соответствующий дескриптор в /proc/$PID/fd/

Хм... Это можно сделать из стороннего процесса?

В конечном счете shm_open() делает ровно тоже самое.

Разве он что-то создаёт на уровне ФС? Мне казалось, там своё пространство имён.

ls-h ★★★★★
() автор топика
Ответ на: комментарий от ls-h

Хм... Это можно сделать из стороннего процесса?

open(«/proc/42/fd/71»)

man proc

Разве он что-то создаёт на уровне ФС? Мне казалось, там своё пространство имён.

man shm_open

The POSIX shared memory object implementation on Linux makes use  of  a
       dedicated tmpfs(5) filesystem that is normally mounted under /dev/shm.

anonymous
()
Ответ на: комментарий от Uncle_Bobby

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

deep-purple ★★★★★
()
Ответ на: комментарий от ls-h

только открывая соответствующий дескриптор в /proc/$PID/fd/

Хм… Это можно сделать из стороннего процесса?

Да, если есть права для доступа. В этом суть /proc и unix-подхода «всё как файл».

В конечном счете shm_open() делает ровно тоже самое.

Разве он что-то создаёт на уровне ФС? Мне казалось, там своё пространство имён.

Нет особого смысла всё это «совсем» изолировать от «/», кроме как чтобы не «писать на диск». А чуть менее чем во всех актуальных unix-ах есть tmpfs и/или O_TMPFILE и т.п.

Deleted
()
Ответ на: комментарий от ls-h
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>


int main(void)
{
	int fd = shm_open("qwerty", O_RDWR|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR);
	if (fd == -1) {
		perror("shm_open failed");
		return 1;
	}

	pid_t pid = getpid();
	printf("my process id is %d\n", pid);

	char link_path[PATH_MAX];
	snprintf(link_path, PATH_MAX, "/proc/%d/fd/%d", pid, fd);
	printf("link_path: %s\n", link_path);

	char real_path[PATH_MAX];
	if (readlink(link_path, real_path, PATH_MAX)== -1) {
		perror("readlink failed");
		return 1;
	}
	printf("real_path: %s\n", real_path);

	write(fd, "hello", 5);

	for (;;) {
		lseek(fd, 0, SEEK_SET);
		char buf[200];
		ssize_t len = read(fd, buf, 200);
		buf[len] = 0;
		puts(buf);
		sleep(1);
	}

	return 0;
}

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

Конпелять cc -lrt file.c

anonymous
()
Ответ на: комментарий от anonymous
ssize_t len = read(fd, buf, 200-1);

исправил

anonymous
()
Ответ на: комментарий от ls-h

https://stackoverflow.com/questions/56077527/why-shm-open-succeeds-even-there...
Ссылку не донёс...

Посмотрел, там начинается чорная магия по подбору альтернативной директории. Тоесть в любом случае либо где-то в файловой системе создастся файл, либо shm_open вернёт ошибку.

Можешь интереса ради запустить прогу выше через strace, потом размонтировать и удалить /dev/shm (в виртуалке, ага), запустить опять и сравнить.

anonymous
()

* Размер/количество буфера(ов) задаются до запуска рабочих процессов. Как правильно реализовать изменение количества/размера после того, как рабочие процессы уже стартовали? Т.е. понятно, как отправить сообщение. Непонятно как закрыть существующий буфер и открыть новый.

shared_ctypes базируется на форке, и потому выделение-высвобождение памяти после форка останется в единственном процессе. Закрыть буфер можно, но нового буфера ты не получишь - он останется в адресном пространстве единственного процесса.

Что у mp.Array под капотом? Я заметил, что python открывает много файлов с именами вида /tmp/pymp-ixc54qx7/pym-27111-h7wi_sy3. Это как-то связано с mp.Array?

shared_ctypes

Возможно как-либо её открыть из другого стороннего процесса, написанного на чём-то ещё? Может быть мне тогда что-то другое использовать, а не mp.Array?

А других готовых решений нет. Можно открывать руками разделяемую память, но это секас еще тот.

Аналогичные вопросы про mp.Pipe():
* Как в работающий процесс передать конец новой трубы?
* Как передать в не Python'овский процесс конец трубы?

mp.Pipe() весьма ограничен, потому я предпочел взять его исходники и переписать под свои нужды.

byko3y ★★★★
()
Ответ на: комментарий от anonymous

То таинственные дедлоки в продакшоне по чётным числам в пятницу

Я вам открою страшную тайну: никто толком не занимается разработкой питона. В принципе, если бы квалифицированные разрабы доработали multiprocessing, то он был бы терпим. Но это никому не нужно.

byko3y ★★★★
()
Ответ на: комментарий от Uncle_Bobby

Если на одной машине из разных процессов то, shared memory. А вообще большие данные и максимально быстро, это не про питон.

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

byko3y ★★★★
()
Ответ на: комментарий от anonymous

Созданный через memfd_create файл ты можешь открыть в паралельном процессе, о чем явно сказано в манах.

memfd_create поддерживается только linux 3.17+.

byko3y ★★★★
()

Вангую что тут какой-нибудь arrow с plasma нужен.

ei-grad ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.