Параллельные процессы.
В составе модулей Python декларируется весьма много средств для запуска нового процесса из кода, многие из которых - альтернатива друг другу:- отдельный модуль subprocess, реализующий класс Popen;
- отдельный модуль popen2, реализующий класс Popen3;
- функции из модуля os группы system(), popen(), popen2(), popen3(), popen4() - выполняющие командную строку в новом экземпляре командного интерпретатора;
- многочисленные функции из модуля os групп exec*() и spawn*() - подменяющие текущий процесс на указанный;
- клонирование процесса вызовом fork() из модуля os;
Все доступные средства запуска новых процессов отчётливо разделяются на три категории: а). запуск экземпляра командного интерпретатора (bash), который запустит процесс (system(), popen(), ...), б). подмена кода текущего адресного процесса на код нового процесса (exec*() и spawn*()), в). создание копии текущего адресного пространства выполняющегося процесса (fork()). Первые две возможности оперируют с двоичным исполнимым форматом в файле, и полностью перекладывают выполнение своей деятельности на функциональность операционной системы. Меня для тестирования больше всего интересовала именно последняя возможность - создание клона процесса вызовом fork() (о причине почему "больше всего" чуть позже). Вот пример подобной программы (файл fork.py):
#!/usr/bin/python
# -*- coding: utf-8 -*-
import os
import time
import sys
import getopt
from rdtsc import rdtsc
delay = 1
procnum = 2
debuglevel = 0
opts, args = getopt.getopt( sys.argv[1:], "p:d:v" )
for opt, arg in opts: # опции (ключи) командной строки
if 0 == cmp( opt[ 1: ], 'p' ): procnum = int( arg )
if 0 == cmp( opt[ 1: ], 'd' ): delay = int( arg )
if 0 == cmp( opt[ 1: ], 'v' ): debuglevel = debuglevel + 1
childs = []
if debuglevel : print "родительский процесс %i" % os.getpid()
for i in range( 0, procnum ) :
tim = rdtsc();
try :
pid = os.fork();
except :
print "error: create child process"
sys.exit( 33 )
if pid == 0 : # в коде дочернего процесса
trun = rdtsc() - tim;
if debuglevel :
print "дочерний процесс %i - циклов процессора на запуск: %u" % \
( os.getpid(), trun );
time.sleep( delay )
trun = rdtsc() - tim;
if debuglevel :
print "дочерний процесс %i - время завершения: %u" % \
( os.getpid(), trun );
sys.exit( 3 )
if pid > 0 : # в коде родительского процесса
childs.append( pid )
if debuglevel :
print "%i: создан новый дочерний процесс %i" % ( os.getpid(), pid )
print "ожидание завершения дочерних процессов ..."
for p in childs :
pid, status = os.wait()
if debuglevel :
print "код завершения процесса %i = %i" % ( pid, os.WEXITSTATUS( status ) )
print "все порождённые процессы успешно завершены"
Вот как выполняется этот код с детализированным уровнем отладочного вывода:
$ ./fork.py -p3 -d2 -v
родительский процесс 17743
17743: создан новый дочерний процесс 17744
17743: создан новый дочерний процесс 17745
17743: создан новый дочерний процесс 17746
ожидание завершения дочерних процессов ...
дочерний процесс 17746 - циклов процессора на запуск: 1136190
дочерний процесс 17745 - циклов процессора на запуск: 2535930
дочерний процесс 17744 - циклов процессора на запуск: 4064020
дочерний процесс 17746 - время завершения: 3330095370
дочерний процесс 17745 - время завершения: 3334456990
дочерний процесс 17744 - время завершения: 3342243510
код завершения процесса 17746 = 3
код завершения процесса 17745 = 3
код завершения процесса 17744 = 3
все порождённые процессы успешно завершены
Здесь много чего интересного: и последовательность старта выполнения созданных процессов, и порядок их завершения работы, и то, что интервал времени до старта создаваемого процесса имеет в точности тот порядок величины времени, что и при запуске потока, а это значит, что основные затраты производительности ложатся не на системные API создания параллельных ветвей выполнения, а на накладные расходы исполняющей системы Python...
А вот его выполнение для очень большого числа порождённых процессов:
$ ./fork.py -p100
ожидание завершения дочерних процессов ...
все порождённые процессы успешно завершены
$ ./fork.py -p500
ожидание завершения дочерних процессов ...
все порождённые процессы успешно завершены
$ ./fork.py -p721
ожидание завершения дочерних процессов ...
все порождённые процессы успешно завершены
$ echo $?
0
$ ./fork.py -p722
error: create child process
$ echo $?
33
Теперь возвратимся к сказанному ранее, что этот случай клонирования процесса самый любопытный... Почему?
- во-первых, потому, что fork() - это самое сердце серверных технологий UNIX на протяжении десятилетий...
- во-вторых, потому, что интересен вопрос: в интерпретирующей (исполняющей) системе Python что клонируется, какой процесс?
$ ./fork.py -p5 -d20
ожидание завершения дочерних процессов ...
все порождённые процессы успешно завершены
$ ps -A | grep 'fork.py'
6882 pts/1 00:00:00 fork.py
6883 pts/1 00:00:00 fork.py
6884 pts/1 00:00:00 fork.py
6885 pts/1 00:00:00 fork.py
6886 pts/1 00:00:00 fork.py
6887 pts/1 00:00:00 fork.py
Как и следовало ожидать, клонируются экзепляры виртуальной машины Python (исполняющей байт-код системы), внутри которых уже, в свою очередь, выполняются копии приложения fork.py.
Ну и, в завершение, другой, более элементарный пример того, как из кода Python запускается новый экземпляр командного интерпретатотора (bash), выполняющий, в свою очередь, заказанное ему приложение:
- родительский процесс (файл parent.py), передающий дочерним процессам работу по поиску вхождений в файлы текстовых фрагментов, заданных переменной word:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import os
import subprocess
import sys
child = os.path.join( os.path.dirname(__file__), "./child.py" )
word = 'word'
file = [ './parent.py', './child.py' ]
pipes = []
for i in range( 0, 2 ):
command = [ sys.executable, child ]
pipe = subprocess.Popen( command, stdin=subprocess.PIPE )
pipes.append( pipe )
pipe.stdin.write( word.encode( "utf8" ) + b"\n" )
pipe.stdin.write( file[ i ].encode( "utf8" ) + b"\n" )
pipe.stdin.close()
while pipes:
pipe = pipes.pop()
pipe.wait()
# -*- coding: utf-8 -*-
import os
import subprocess
import sys
child = os.path.join( os.path.dirname(__file__), "./child.py" )
word = 'word'
file = [ './parent.py', './child.py' ]
pipes = []
for i in range( 0, 2 ):
command = [ sys.executable, child ]
pipe = subprocess.Popen( command, stdin=subprocess.PIPE )
pipes.append( pipe )
pipe.stdin.write( word.encode( "utf8" ) + b"\n" )
pipe.stdin.write( file[ i ].encode( "utf8" ) + b"\n" )
pipe.stdin.close()
while pipes:
pipe = pipes.pop()
pipe.wait()
- порождённый процесс (файл child.py), причём, и то что искать (сигнатуру) и то где искать (имя файла для поиска) этот процесс получает из входного потока от своего родительского процесса:
# -*- coding: utf-8 -*-
import sys
word = sys.stdin.readline().rstrip()
filename = sys.stdin.readline().rstrip()
try:
with open( filename, "rb" ) as fh:
while True:
current = fh.readline()
if not current:
break
if ( word in current ):
print( "find: {0} {1}".format( filename, word ) )
except :
pass
И вот что, в итоге, мы увидим:
$ ./parent.py
find: ./parent.py word
find: ./parent.py word
find: ./child.py word
find: ./child.py word
find: ./child.py word