понедельник, 1 июля 2013 г.

Python: примеры и тесты, часть 5 - процессы

Параллельные процессы.

В составе модулей Python декларируется весьма много средств для запуска нового процесса из кода, многие из которых - альтернатива друг другу:
  • отдельный модуль subprocess, реализующий класс Popen;
  • отдельный модуль popen2, реализующий класс Popen3;
  • функции из модуля os группы system(), popen(), popen2(), popen3(), popen4() - выполняющие командную строку в новом экземпляре командного интерпретатора;
  • многочисленные функции из модуля os групп exec*() и spawn*() - подменяющие текущий процесс на указанный;
  • клонирование процесса вызовом fork() из модуля os; 
Наверняка существует ещё множество альтернативных реализаций, представленных сторонними производителями, что обуславливается лёгкостью тиражирования программного обеспечения для Python.

Все доступные средства запуска новых процессов отчётливо разделяются на три категории: а). запуск экземпляра командного интерпретатора (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()
  • порождённый процесс (файл child.py), причём, и то что искать (сигнатуру) и то где искать (имя файла для поиска) этот процесс получает из входного потока от своего родительского процесса:
#!/usr/bin/python
# -*- 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



Комментариев нет: