Python进阶(十)subprocess模块、多进程multiprocessing

文章目录

  • subprocess模块
    • subprocess.run() 函数的使用
    • subprocess.Popen()函数
    • 实时获取subprocess子进程的输出
  • 多进程multiprocessing
    • 为什么要多进程?
    • 多进程实现方法
    • 进程间通信之Queue
    • 进程间通信之Pipe
    • os.system接口

   进程是资源分配的最小单位, 线程是CPU调度的最小单位。做个简单的比喻:进程=火车,线程=车厢。

subprocess模块

  Pythonsubprocess模块,用来创建和管理子进程(不是线程),并能够与创建的子进程的stdinstdoutstderr连接通信,获取子进程执行结束后的返回码,在执行超时或执行错误时得到异常。

  从Python3.5版本开始,subprocess模块内部又进行了一次整合 ,最后就剩下官方推荐的两个接口函数,分别是:

subprocess.run()
subprocess.Popen()

  考虑到这个模块对外接口的函数和对象名称都比较特别,本文就这样来引入:

>>> from subprocess import *
>>> dir()
['CalledProcessError', 'CompletedProcess', 'DEVNULL', 'PIPE', 'Popen', 'STDOUT', 'SubprocessError', 'TimeoutExpired', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'call', 'check_call', 'check_output', 'getoutput', 'getstatusoutput', 'run']

  callcheck_callcheck_outputgetoutputgetstatusoutput这些函数,都被run函数代替了,它们在存在只是为了保持向下兼容。

subprocess.run() 函数的使用

  从Python3.5开始,出现了run函数,用来代替之前版本的一些函数接口。run函数的作用是:执行args参数所表示的命令,等待命令执行完毕,返回一个CompletedProcess对象。

  注意:run函数是同步函数,要等待

  • 同步函数:当一个函数是同步执行时,那么当该函数被调用时不会立即返回,直到该函数所要做的事情全都做完了才返回。
  • 异步函数:如果一个异步函数被调用时,该函数会立即返回尽管该函数规定的操作任务还没有完成。

  run()函数的接口参数:

subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, 
capture_output=False, shell=False, cwd=None, timeout=None, 
check=False, encoding=None, errors=None, text=None, env=None,
 universal_newlines=None)
  1. args参数,就是要通过创建进程而执行的命令及参数,run函数通过args来创建一个进程并执行。

  2. shell参数,表示是否通过shell来执行命令(Linux下默认为/bin/sh),默认是False,这时args只能是一个不带参数的命令字符串,或者是命令和参数组成的一个list,如果shell=Trueargs就可以是一个我们常见的命令字符串。

>>> run('ls')
>>> run(['ls','-lh'])
>>> run('ls -lh', shell=True)

  注意run函数返回的CompletedProcess对象,里面包含了args,以及命令执行的返回码。下面的代码示例,说明了访问CompletedProcess对象的方式:

>>> proc = run('ls')
Desktop    Downloads	     Music     Public	  test
Documents  examples.desktop  Pictures  Templates  Videos
>>> proc.args
'ls'
>>> proc.returncode
0

  input参数,命令的具体输入内容,默认None,表示没有输入。inputstdin不能同时使用。先看一个有input参数的例子:

>>> proc = run('grep fs',shell=True,input=b'adfs\ncccc\nfsfsf')
adfs
fsfsf
>>> proc
CompletedProcess(args='grep fs', returncode=0)
# input默认是一个bytes流。
  1. stdin参数:指定命令的输入途径;
>>> f = open('tt.t','r')
>>> proc = run('cat -n', shell=True, stdin=f)
     1	12345
     2	abcde
     3	xyz..
>>> f.close()
  1. stdout参数:指定命令的输出途径;默认为None,如上面的代码示例,输出就直接打印出来了;
>>> proc = run('grep fs',shell=True,input=b'adfs\ncccc\nfsfsf',stdout=PIPE)
>>> proc
CompletedProcess(args='grep fs', returncode=0, stdout=b'adfs\nfsfsf\n')
>>> proc.stdout
b'adfs\nfsfsf\n'

  stdout=PIPE,表示将stdout重定向到管道,用了这个参数,grep fs命令的结果,就不会直接打印出来,而是存入了proc.stdout这个管道内。

  1. stderr参数:指定命令的error输出途径;
>>> proc = run('ls fs',shell=True,stdout=PIPE,stderr=PIPE)
>>> proc.stdout
b''
>>> proc.stderr
b"ls: cannot access 'fs': No such file or directory\n"

  看一个stdoutinput配合起来使用的例子,有点像我们在Linux shell输入的有管道的命令行:

>>> proc = run('grep fs',shell=True,input=b'adfs\ncccc\nfsfsf',stdout=PIPE)
>>> run('cat -n',shell=True, input=proc.stdout)
     1	adfs
     2	fsfsf
CompletedProcess(args='cat -n', returncode=0)

  把stderr重定向到stdout

>>> proc = run('ls kk', shell=True, stdout=PIPE, stderr=STDOUT)
>>> proc.stdout
b"ls: cannot access 'kk': No such file or directory\n"
  1. capture_output参数:这个参数顾名思义就是捕获进程的输出,stdoutstderrcapture_output=True的效果与设置stdout=PIPE, stderr=PIPE一样。设置了capture_output=True,就不能再设置stdout和stderr:
>>> proc = run('ls kk', shell=True, capture_output=True)
>>> proc
CompletedProcess(args='ls kk', returncode=2, stdout=b'', stderr=b"ls: cannot access 'kk': No such file or directory\n")
>>> proc.stdout
b''
>>> proc.stderr
b"ls: cannot access 'kk': No such file or directory\n"
  1. cwd参数:这个参数指示了当前工作路径:
>>> proc = run('ls -lh', shell=True, cwd='/usr/local')
total 36K
drwxr-xr-x 2 root root 4.0K Feb  9 16:12 bin
drwxr-xr-x 2 root root 4.0K Feb  9 16:12 etc
drwxr-xr-x 2 root root 4.0K Feb  9 16:12 games
drwxr-xr-x 2 root root 4.0K Feb  9 16:12 include
drwxr-xr-x 3 root root 4.0K Jun 28 21:54 lib
lrwxrwxrwx 1 root root    9 Jun 28 21:32 man -> share/man
drwxr-xr-x 6 root root 4.0K Jun 28 23:34 python-3.7.3
drwxr-xr-x 2 root root 4.0K Feb  9 16:12 sbin
drwxr-xr-x 6 root root 4.0K Feb  9 16:15 share
drwxr-xr-x 2 root root 4.0K Feb  9 16:12 src
  1. text参数,universal_newlines参数

  这两个参数的作用是一样的,universal_newlines这个参数的存在也是为了向下兼容(Python3.7开始有text参数,3.53.6都是universal_newlines参数),因此我们使用text就好了。text参数的作用是,将stdinstdoutstderr修改为string模式。上面的示例代码,都是bytes流:

>>> run('grep fs', shell=True, input=b'asdfs\nfdfs', capture_output=True)
CompletedProcess(args='grep fs', returncode=0, stdout=b'asdfs\nfdfs\n', stderr=b'')
>>> run('grep fs', shell=True, input='asdfs\nfdfs', capture_output=True, text=True) 
CompletedProcess(args='grep fs', returncode=0, stdout='asdfs\nfdfs\n', stderr='')
  1. timeout参数:设置进程执行的超时时间。如果时间到子进程还未结束, subprocess.TimeoutExpired异常会抛出。timeout参数的单位是秒。
>>> try:
...     run('python3', shell=True, input=b'import time;time.sleep(30)', timeout=1)
... except TimeoutExpired:
...     print('timeout happened...')
... 
timeout happened...

以上代码,就是sleep 30秒,run函数设置timeout为1秒,触发subprocess.TimeoutExpired后,打印一点信息出来。

  1. check参数:如果check=True,在子进程的返回不为0的时候,抛出subprocess.CalledProcessError异常。这时,run函数返回的CompletedProcess对象的returncode不可用。
>>> try:
...     proc = run('ls kk', shell=True, check=True, stderr=PIPE)
... except CalledProcessError:
...     print(proc.returncode)
... 
0

  上面这段代码,走到了except里面,因为kk目录不存在,但是打印出来的returncode却是0run函数没有成功返回,而是抛出异常,因此返回值不可用。

subprocess.Popen()函数

  run函数的底层,就是Popen函数。run函数是同步的,要等待子进程实行结束,或者超时。Popen创建子进程后,采用异步的方式,不会等待,要通过poll函数来判断子进程是否执行完毕。

Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, 
preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, 
universal_newlines=None, startupinfo=None, creationflags=0, 
restore_signals=True, start_new_session=False, pass_fds=(), *,
 encoding=None, errors=None, text=None)

  参数argsstdinstdoutstderrshellcwduniversal_newlinestextrun函数的含义和用法都是一样的。

  1. Popen函数的基本用法
>>> proc = Popen('ls -hl', shell=True, stdout=PIPE, stderr=STDOUT)
>>> out, _ = proc.communicate()
>>> print(out.decode())
total 37M
-rw-r--r--  1 xinlin xinlin  535 Jun 29 06:03 apache_log_reader.py
-rw-r--r--  1 xinlin xinlin 3.2M Jun 30 02:55 py.maixj.sql
-rw-r--r--  1 xinlin xinlin 3.2M Jun 29 19:20 py.online.sql
drwxr-xr-x 19 xinlin xinlin 4.0K Jun 28 23:24 Python-3.7.3
-rw-r--r--  1 xinlin xinlin  22M Mar 25 13:59 Python-3.7.3.tgz
-rw-r--r--  1 xinlin xinlin   27 Jul  5 01:05 sleep.py
-rw-r--r--  1 xinlin xinlin   18 Jul  5 00:10 tt.t
-rw-r--r--  1 xinlin xinlin  800 Jun 29 03:26 walktree.py
-rw-r--r--  1 xinlin xinlin 8.2M Jun 29 05:47 www.access_log_2019_06_28
>>> proc.returncode
0
>>> proc.pid
2985

  Popen函数以异步的方式创建一个子进程,返回一个Popen对象。我们通过communicate函数来获取stdoutstderrcommunicate函数返回一个tuple,以上示例是将stderr=STDOUT,因此使用 _ 来表示为空的stderr

  因为subprocess模块是用调用shell命令的方式创建进程,我们可以直接用这一行shell命令启动后台进程:

import subprocess
subprocess.Popen('python3.8 start.py', shell=True)
  1. 设置子进程的输入和超时时间

  Popen对象的communicate函数有两个参数,inputtimeout,分别用来设置给子进程的输入和超时时间。有timeout参数,表示communicate函数会等待子进程执行结束,或者超时。

>>> proc = Popen('grep fs', shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
>>> out, err = proc.communicate(b'adfs\nfsmnjkl')
>>> out
b'adfs\nfsmnjkl\n'
>>> err
b''

  1. Popen以异步的方式创建子进程,创建时可以设定stdinstdoutstderr全部指向PIPE,此时子进程的输入输出全部都在管道中,就像我们再shell命令行直接使用管道(|)一样!

  2. 在使用管道(|)连接多个程序的时候,前一个程序的输出成为了后一个程序的输入,此时如果假设后一个程序时通过subprocessPopen创建的,那么此时此子进程的stdin,就是前一个程序的输出,而它的stdoutstderr,通过communicate函数,可以直接获得。

  通过子进程的communicate函数,我们可以像使用shell的管道一样,直接连接多个程序的输入和输出;但是,这种输入和输出,也跟shell管道一样,是一次性的;即如果某个程序有运行时会连续多次获取输入,communicate就无能为力(此时就要使用pexpect)。

  再来一个有timeout的例子:

>>> proc = Popen('python3', shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
>>> try:
...     out,err=proc.communicate(b'import time;time.sleep(30)', 1)
... except TimeoutExpired: 
...     print('time out...')
... 
time out...

  Popen对象有一个 wait成员函数,也可以设置一个timeout来等待子进程的结束:

>>> try:
...     proc=Popen('python3 -c "import time;time.sleep(30)"',shell=True,stdout=PIPE)
...     returncode = proc.wait(15)
... except TimeoutExpired:
...     print('after waiting 15 seconds, timeout finally...')
... 
after waiting 15 seconds, timeout finally...

  注意对returncode的赋值,如果timeout发生,returncode就是not defined。当然也可以通过proc.returncode来获取。如果异常,proc.returncode的值是None

  如果想确定子进程执行的时长,可以采用poll函数:

>>> def test_Popen():
...     import time
...     proc=Popen('python3 -c "import time;time.sleep(10)"',shell=True,stdout=PIPE)
...     i = 0
...     while True:
...         returncode = proc.poll()
...         if returncode is None:
...             time.sleep(2)
...             i += 2
...             print('sleep',i,'seconds')
...             continue
...         else:
...             print('sub process is terminated with returncode',returncode)
...             break
... 
>>> test_Popen()
sleep 2 seconds
sleep 4 seconds
sleep 6 seconds
sleep 8 seconds
sleep 10 seconds
sleep 12 seconds
sub process is terminated with returncode 0

  Popen对象还有下列几个成员函数:

Popen.send_signal(signal)
Popen.terminate()
Popen.kill()
  • 更多参考官方文档:https://docs.python.org/3/library/subprocess.html

实时获取subprocess子进程的输出

大体思路:既然是实时获取subprocess创建的子进程的输出,我们就不能使用run,而要使用Popen创建异步子进程,然后通过poll函数来查看异步子进程的执行状态,如果执行没有结束,我们就直接去读取子进程的stdout,然后在主进程中处理。

  1. 子进程代码,代码在subproc.py文件中:
import time
while True:
    print('subprocess print...', flush=True)
    time.sleep(1)

注意:print函数一定要使用flush=True,否则子进程的输出都在缓存中,主进程也无法读取出来!

  1. 主进程代码,在proc.py文件中:
from subprocess import *
proc = Popen('python subproc.py', shell=True, stdout=PIPE, stderr=PIPE)
while True:
    rcode = proc.poll()
    if rcode is None:
        print('from subprocess: ', end='')
        line = proc.stdout.readline().strip()
        print(line.decode())
  1. 两个.py文件放在同一目录下,下面是执行效果:
E:\py>python proc.py
from subprocess: subprocess print...
from subprocess: subprocess print...
from subprocess: subprocess print...
from subprocess: subprocess print...
from subprocess: subprocess print...
from subprocess: Traceback (most recent call last):
  File "proc.py", line 10, in 
    line = proc.stdout.readline().strip()
KeyboardInterrupt

  用这个技术,可以在python命令行程序上套一层GUI的壳!在GUI程序中,用subprocess以子进程的方式启动命令行程序,实时获取子进程的输出并在GUI中显示出来。

不过有个问题,原命令行程序的print如果没有加flush=True参数怎么办?可以使用python命令行的-u参数!

如果subproc.py的代码是这样的,即print没有flush=True:

import time
while True:
    print('subprocess print...')
    time.sleep(1)

proc.py的代码修改如下,使用 -u 参数:

from subprocess import *
proc = Popen('python -u subproc.py', shell=True, stdout=PIPE, stderr=PIPE)
while True:
    rcode = proc.poll()
    if rcode is None:
        print('from subprocess: ', end='')
        line = proc.stdout.readline().strip()
        print(line.decode())

多进程multiprocessing

  python标准库中有一个多进程模块,multiprocesing,它可以支持在代码创建多个进程协同运行的计算模型,此模块很多接口名和参数,都与多线程一致。

从代码上看多进程,依然是给进程指定一个函数作为入口,python底层自动启动一个独立进程从此入口开始执行。如果是GUI程序,多进程可以更好的实现多个tkinter的root窗口。

为什么要多进程?

  CPython解释器中的GIL(Global Intercepto Lock,全局解释器锁GIL 并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念),限制了多线程无法充分利用多CPU资源,因此有了多进程的用武之地。如果IO密集型的任务,多线程就可以了。如果是计算密集型的任务,有多个CPU时,就要考虑多进程,以充分利用资源。

  • 并行和并发并行和并发同属于多任务,目的是要提高CPU的使用效率。这里需要注意的是,一个CPU永远不可能实现并行,即一个CPU不能同时运行多个程序,但是可以在随机分配的时间片内交替执行(并发),就好像一个人不能同时看两本书,但是却能够先看第一本书半分钟,再看第二本书半分钟,这样来回切换。

  Guido van Rossum(吉多·范罗苏姆)创建Python时就只考虑到单核CPU,解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁,于是有了GIL这把超级大锁。因为cpython解析只允许拥有GIL全局解析器锁才能运行程序,这样就保证了保证同一个时刻只允许一个线程可以使用cpu。由于大量的程序开发者接收了这套机制,现在代码量越来越多,已经不容易通过C代码去解决这个问题。

  每个线程在执行时候都需要先获取GIL,保证同一时刻只有一个线程可以执行代码,即同一时刻只有一个线程使用CPU,也就是说多线程并不是真正意义上的并行执行。

那么,我们改如何解决GIL锁的问题呢?

  1. 更换cpython为jpython(不建议)
  2. 使用多进程完成多线程的任务
  3. 在使用多线程可以使用c语言去实现

问题1: 什么时候会释放Gil锁?

  1. 遇到像 i/o操作这种 会有时间空闲情况 造成cpu闲置的情况会释放Gil
  2. 会有一个专门ticks进行计数 一旦ticks数值达到100 这个时候释放Gil锁 线程之间开始竞争Gil锁(说明:ticks这个数值可以进行设置来延长或者缩减获得Gil锁的线程使用cpu的时间)

问题2: 互斥锁和Gil锁的关系

Gil锁 : 保证同一时刻只有一个线程能使用到CPU。
互斥锁 : 多线程时,保证修改共享数据时有序的修改,不会产生数据修改混乱。
GIL与多线程:有了GIL的存在,Python有这两个特点:

  1. 进程可以利用多核,但是开销大。
  2. 多线程开销小,却无法利用多核优势。

  也就是说Python中的多线程是假的多线程,Python解释器虽然可以开启多个线程,但同一时间只有一个线程能在解释器中执行,而做到这一点正是由于GIL锁的存在,它的存在使得CPU的资源同一时间只会给一个线程使用,而由于开启线程的开销小,所以多线程才能有一片用武之地,不然就真的是鸡肋了。

多进程实现方法

  1. 通过Python的Multiprocessing模块创建多进程的计算模型
from multiprocessing import  Process
import time
import random
def fun1(i):
    time.sleep(random.randint(0,2))
    print('multiprocess test %d' %i)

if __name__ == '__main__':
    process_list = []
    for i in range(5):  #开启5个子进程执行fun1函数
        p = Process(target=fun1,args=(i,)) #实例化进程对象
        p.start()
        process_list.append(p)

    for p in process_list:
        p.join()

    print('Done!')

  通过multiprocessing模块的Process创建进程对象,然后startjoin用来阻塞等待进程执行结束。接口基本与多线程一致。

  多进程的入口也就是一个普普通通的函数!但是它是以独立进程的方式运行。

以上代码执行效果:

D:\py>python mptest.py
multiprocess test 1
multiprocess test 4
multiprocess test 0
multiprocess test 2
multiprocess test 3
Done!

D:\py>python mptest.py
multiprocess test 0
multiprocess test 2
multiprocess test 3
multiprocess test 1
multiprocess test 4
Done!
  1. 用继承的方法创建进程
from multiprocessing import  Process
import time
import random

class MyProcess(Process): #继承Process类
    def __init__(self,name):
        super(MyProcess,self).__init__()
        self.name = name

    def run(self):
        time.sleep(random.randint(0,2))
        print('测试多进程 %s' % self.name)

if __name__ == '__main__':
    process_list = []
    for i in range(5):  #开启5个子进程执行fun1函数
        p = MyProcess(str(i)*8) #实例化进程对象
        p.start()
        process_list.append(p)

    for p in process_list:
        p.join()

    print('Done!')

以上代码执行效果:

D:\py>python mptest.py
测试多进程 11111111
测试多进程 33333333
测试多进程 00000000
测试多进程 22222222
测试多进程 44444444
Done!

D:\py>python mptest.py
测试多进程 22222222
测试多进程 11111111
测试多进程 44444444
测试多进程 00000000
测试多进程 33333333
Done!

进程间通信之Queue

  multiprocess.Queue,是提供个python多进程见通信使用的。python有一个好的设计,即多线程和多进程的接口基本相同,现在这两个Queue的使用接口也基本相同:

from multiprocessing import Process, Queue
def f(q):
    q.put([42, None, 'hello'])

def g(q):
    q.put([48, None, 'hello gggg'])

def h(q):
    print(q.get())
    print(q.get())

if __name__ == '__main__':
    q = Queue()
    p1 = Process(target=f, args=(q,))
    p2 = Process(target=g, args=(q,))
    p3 = Process(target=h, args=(q,))
    p1.start()
    p2.start()
    p3.start()
    p1.join()
    p2.join()
    p3.join()

主进程开了3个子进程,两个做put,一个做get。运行结果:

[42, None, 'hello']
[48, None, 'hello gggg']

进程间通信之Pipe

  相对于multiprocess.Queuepipe更快,但是不适合多个进程间的通信:

from multiprocessing import Process, Pipe

def f(conn):
    conn.send([42, None, 'hello'])
    for i in range(2):
        print(conn.recv())

if __name__ == '__main__':
    parent_conn, child_conn = Pipe()
    p = Process(target=f, args=(child_conn,))
    p.start()
    print("parent_conn_recv: ", parent_conn.recv())
    parent_conn.send('i am your father!!')
    parent_conn.send('boy...')
    p.join()

  多进程通信的Pipe管道构建的时候返回两个对象,这两个对象之间可以通过sendrecv方法传递数据。

  以上代码执行效果:

parent_conn_recv:  [42, None, 'hello']
i am your father!!
boy...

  如上示例,test.py的执行,就是启动一个新的python进程,然后自己就退出了。这个新的Python进程,就成了后台进程,logout后也依然存在。

os.system接口

  os.system底层调用的C标准接口system,它只能返回子进程的返回码,而无法与子进程进行复杂交互。我们就简单理解它可以直接在python代码中执行shell命令吧,子进程的输出会直接出现的父进程的stdout内。

>>> a = os.system('ls -ahl')
total 56K
drwxr-xr-x 5 pi pi 4.0K Jul  3 14:22 .
drwxr-xr-x 5 pi pi 4.0K Jun 24 12:00 ..
-rw-r--r-- 1 pi pi 2.9K Jun 18 20:49 common.py
-rw-r--r-- 1 pi pi  164 Jun 18 20:49 config.ini
drwxr-xr-x 8 pi pi 4.0K Jul  3 10:01 .git
drwxr-xr-x 2 pi pi 4.0K Jul  3 10:00 __pycache__
-rw-r--r-- 1 pi pi   48 Jun  7 22:17 README.md
-rw-r--r-- 1 pi pi  603 Jun 18 20:49 start.py
-rw-r--r-- 1 pi pi  16K Jul  3 10:00 tcp_server.py
drwxr-xr-x 2 pi pi 4.0K Jul  2 10:48 test
-rw-r--r-- 1 pi pi 2.1K Jun 18 20:49 udp_hb.py
>>> a
0

  关于子进程的创建需要明确两点: (1)父进程的环境变量(environment variables)会默认传递到子进程中(工作目录PWD就是环境变量之一) (2)使用os.system函数,子进程无法影响父进程中的环境变量。

  有时我们在python中调试代码的时候,还是挺方便的。比如上面的示例,查看一下某个路径的文件列表,或者在不离开当前python环境的情况下,启动一个后台进程。

你可能感兴趣的:(Python进阶(完结))