目录
- 1. subprocess
- 1.1. run(), 阻塞调用
- 1.1.1. shell选项
- 1.1.2. 获取输出
- 1.1.3. check选项
- 1.2. call(), 旧版本函数
- 1.3. Popen, 非阻塞
- 1.3.1. 管理子进程(通信)
- sys.stdin
- 示例
- 2. multiprocess
- 2.1. 创建子进程
- 2.1.1. 直接使用Process模块创建进程
- 2.1.2. 继承的形式创建进程
- 2.1.3. Process模块方法介绍
- 2.2. 进程管理
- 2.2.1. 僵尸进程(有害)
- 2.2.2. 孤儿进程(无害)
- 2.2.3. 守护进程
- 2.3. 进程同步
- 2.4. 进程池
- 2.5. 管道:进程间通讯
- 2.6. Manager模块:进程间数据共享
- 2.7. multiprocessing.Queue
两个模块的对比:
- multiprocess: 是同一个代码中通过多进程调用其他的模块(也是自己写的)
- subprocess: 直接调用外部的二进制程序,而非代码模块
1. subprocess
homepage
该模块主要用于调用子程序
。
1.1. run(), 阻塞调用
subprocess.call("mv abc", shell=True) # depressed after python3.5
subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, shell=False, timeout=None, check=False) -> subprocess.CompletedProcess
run/call()
函数均为主进程阻塞执行子进程,直到子进程调用完成返回;
1.1.1. shell选项
如果 shell=True
,则将通过shell执行指定的命令。如果你希望方便地访问其他shell功能,如shell管道、文件名通配符、环境变量扩展和扩展,例如 ~
到用户的主目录,这会很有用。
# subprocess.run("ls | grep xxx".split(" "))
# 以上语句,无法获取到想要的结果,因为subprocess本身并不执行 `|` 管道符
subprocess.run("ls | grep xxx".split(" "), shell=True)
1.1.2. 获取输出
proc = subprocess.run(['uname', '-r'], stdout=su.PIPE)
print(proc.returncode)
print(proc.stdout)
1.1.3. check选项
当设置 check=True
时,如果returncode返回非0,则抛出异常 CalledProcessError
。
If check is True and the exit code was non-zero, it raises a
CalledProcessError
. TheCalledProcessError
object will have the return code in the returncode attribute, and output & stderr attributes if those streams were captured.
1.2. call(), 旧版本函数
如果你不得不在Python3.5之前的版本运行,那么你可以选用以下几种方式,来替代 subprocess.run()
的调用。
subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None, **other_popen_kwargs)
如果希望在 call()
处理过程中使用 check=True
,则直接使用 check_call()
函数,它等效于 run(..., check=True)
。
Note Do not use
stdout=PIPE
orstderr=PIPE
with ## this function.
那么如何获取到 stdout
呢?
subprocess.check_output(args, *, stdin=None, stderr=None, shell=False, cwd=None, encoding=None, errors=None, universal_newlines=None, timeout=None, text=None, **other_popen_kwargs)
This is equivalent to: run(..., check=True, stdout=PIPE).stdout
也可以捕获到stderr,使用 stderr=subprocess.STDOUT
:
>>> subprocess.check_output(
... "ls non_existent_file; exit 0",
... stderr=subprocess.STDOUT,
... shell=True)
'ls: non_existent_file: No such file or directory\n'
1.3. Popen, 非阻塞
homepage
简单使用示例:
import subprocess as sub
>>> ls = sub.Popen(['ls'], stdout=sub.PIPE)
>>> for f in ls.stdout: print(f)
...
b'fileA.txt\n'
b'fileB.txt\n'
b'ls.txt\n'
>>> ex = sub.Popen(['ex', 'test.txt'], stdin=sub.PIPE)
>>> ex.stdin.write(b'i\nthis is a test\n.\n')
19
>>> ex.std.write(b'wq\n')
3
注意: shell=True
选项会开启Windows控制台执行命令,这会引发一个问题——Windows中的控制台命令是后台执行的!所以当python调用 Popen.terminate()
时,只是关闭了控制台,控制台命令却仍在后台执行(直至结束)。所以,如需关闭时同时关闭命令,请不要使用 Popen(str_cmd, shell=True)
,用 Popen(str_cmd.split(' ')
代替。
p.poll() # 检查进程是否终止,如果终止返回returncode,否则返回None
p.wait(timeout) # 等待子进程终止(阻塞父进程)
p.communicate(input,timeout) # 和子进程交互,发送和读取数据(阻塞父进程)
p.send_signal(singnal) # 发送信号到子进程
p.terminate() # 停止子进程,也就是发送SIGTERM信号到子进程
p.kill() # 杀死子进程。发送SIGKILL信号到子进程
创建Popen对象后,主程序不会自动等待子进程完成。
以上三个成员函数都可以用于等待子进程返回:while循环配合Popen.poll()、Popen.wait()、Popen.communicate()。由于后面二者都会阻塞父进程,所以无法实时获取子进程输出,而是等待子进程结束后一并输出所有打印信息。另外,Popen.wait()、Popen.communicate()分别将输出存放于管道和内存,前者容易超出默认大小而导致死锁,因此不推荐使用。
注意:p.communicate(stdin="xxx")
该函数会终止子程序(因为其是阻塞的,当父程序解除阻塞时,意味着子程序已经结束了)。所以,如果你的子程序是 while...
或者 for line in sys.stdin
时,你会发现子程序意外的结束了,而不是在循环中等待。
1.3.1. 管理子进程(通信)
Popen类具有三个与输入输出相关的属性:Popen.stdin
, Popen.stdout
和 Popen.stderr
,分别对应子进程的标准输入/输出/错误。它们的值可以是PIPE、文件描述符(正整数)、文件对象或None:
- PIPE表示创建一个连接子进程的新管道,默认值None, 表示不做重定向。
- 子进程的文件句柄可以从父进程中继承得到。
- 仅
stderr
可以设置为STDOUT
,表示将子进程的标准错误重定向到标准输出。
child1 = subprocess.Popen(["ls","-l"], stdout=subprocess.PIPE)
child2 = subprocess.Popen(["wc"], stdin=child1.stdout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out = child2.communicate()
其中,subprocess.PIPE为文本流提供一个缓存区,child1的stdout将文本输出到缓存区;随后child2的stdin从该PIPE读取文本,child2的输出文本也被存放在PIPE中,而标准错误信息则重定向到标准输出;最后,communicate()方法从PIPE中读取child2子进程的标准输出和标准错误。
注意:subprocess.stdxxx
操作bytes字节,而 sys.stdin
则是string。
sys.stdin
Python的sys模块定义了标准输入/输出/错误:
sys.stdin # 标准输入
sys.stdout # 标准输出
sys.stderr # 标准错误信息
以上三个对象类似于文件流,因此可以使用readline()和write()方法进行读写操作。也可以使用print(),等效于sys.stdout.write()。
需要注意的是,除了直接向控制台打印输出外,标准输出/错误的打印存在缓存,为了实时输出打印信息,需要执行
sys.stdout.flush()
sys.stderr.flush()
读取 sys.stdin
的方式:
for line in sys.stdin:
print(type(line)) # string
...
示例
前面提到,如果子程序只是调用一次,并获取其输出状态,可以使用 p.communicate(stdin, timeout)
结合 p.returncode
实现。
但如果你的程序想实现类似 TCP-C/S 式的持续通信服务,这里提供一个Demo:
父程序:
proc = subprocess.Popen(command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
# stderr=subprocess.STDOUT)
# try:
while proc.poll() is None: # 持续输入
str_input = input("Please Input a path: ")
if str_input == "quit":
break
bytes_path = f"/home/brt/{str_input}.jpg\n".encode() # 注意这里需要\n换行符
proc.stdin.write(bytes_path) # 需要使用bytes
proc.stdin.flush()
# proc.communicate(stdin=str_input, timout=5)
bytes_state = proc.stdout.readline() # bytes
if bytes_state == b"ok\n":
print("Well Done.")
# except subprocess.TimeoutExpired:
# print("子程序Timout未响应...")
# break
# if proc.poll() is None: # communicate()超时时,子程序可能未退出
# proc.kill()
子程序:
for path_save in sys.stdin: # 持续读取
path_save = path_save.strip() # 删除多余的换行符
img = grabclipboard_byQt(cb)
# sys.stderr.write(">>>", img)
if img:
save_clipboard_image(img, path_save)
str_pipe = "ok"
else:
str_pipe = ""
# 以下内容用于写入stdout管道,向父程序反馈
# sys.stdout.write(str_pipe + "\n") # 必须添加换行符
print(str_pipe)
sys.stdout.flush() # 及时清空缓存
2. multiprocess
2.1. 创建子进程
2.1.1. 直接使用Process模块创建进程
Process([group [, target [, name [, args [, kwargs]]]]])
- group: 参数未使用,值始终为None
- target: 表示调用对象,即子进程要执行的任务
- name: 子进程的名称
- args: 调用对象的位置参数元组,args=(1,2,'egon',)
- kwargs: 调用对象的字典,kwargs=
举例说明:
from multiprocessing import Process
def func():
print("子进程正在运行")
time.sleep(1)
print("子进程ID>>>", os.getpid())
print("子进程的父进程ID>>>", os.getppid())
if __name__ == '__main__':
p = Process(target=func,)
p.start()
print("父进程ID>>>", os.getpid())
print("父进程的父进程ID>>>", os.getppid())
p.join()
print("主进程执行完毕!")
2.1.2. 继承的形式创建进程
from multiprocessing import Process
class MyProcess(Process):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print("子进程的名字是>>>", self.name)
time.sleep(2)
print("子进程结束")
if __name__ == '__main__':
p = MyProcess(123, name="子进程01")
p.start()
print("p.name:", p.name, "\np.pid:", p.pid)
print("p.is_alive:", p.is_alive())
p.terminate() # 给操作系统发送一个关闭进程p1的信号,让操作系统去关闭它
p.join()
print("主进程结束")
2.1.3. Process模块方法介绍
- p.start(): 启动进程,并调用该子进程中的p.run()
- p.run(): 进程启动时运行的方法,正是它去调用target指定的函数
- p.terminate(): 强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
- p.is_alive(): 如果p仍然运行,返回True
- p.join([timeout]): 主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)
2.2. 进程管理
2.2.1. 僵尸进程(有害)
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
2.2.2. 孤儿进程(无害)
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
2.2.3. 守护进程
使用平常的方法时,子进程是不会随着主进程的结束而结束,只有当主进程和子进程全部执行完毕后,程序才会结束.但是,如果我们的需求是: 主进程执行结束,由该主进程创建的子进程必须跟着结束. 这时,我们就需要用到守护进程了.
主进程创建守护进程:
- 守护进程会在主进程代码执行结束后就终止
- 守护进程内无法再开启子进程,否则抛出异常: AssertionError: daemonic processes are not allowed to have children
需要注意的是: 进程之间是相互独立的,主进程代码运行结束,守护进程随机终止.
class Myprocess(Process):
def __init__(self,person):
super().__init__()
self.person = person
def run(self):
print("这个人的ID号是:%s" % os.getpid())
print("这个人的名字是:%s" % self.name)
time.sleep(3)
if __name__ == '__main__':
p = Myprocess('李华')
p.daemon=True #一定要在p.start()前设置,设置p为守护进程,禁止p创建子进程,并且父进程代码执行结束,p即终止运行
p.start()
# time.sleep(1) # 在sleep时linux下查看进程id对应的进程ps -ef|grep id
print('主进程执行完毕!')
2.3. 进程同步
cnblog: Python multiprocess模块(中)
2.4. 进程池
cnblog: Python multiprocess模块(下)
2.5. 管道:进程间通讯
管道(不推荐使用)是进程间通信(IPC)的方式,但它可能导致数据不安全的情况出现。
#创建管道的类:
Pipe([duplex]): 在进程之间创建一条管道, 并返回元组(conn1, conn2), 其中conn1, conn2表示管道两端的连接对象. 强调一点: 必须在产生Process对象之前产生管道.
#参数介绍:
dumplex: 默认管道是全双工的, 如果将duplex设置成False, conn1只能用于接收, conn2只能用于发送.
#主要方法:
conn1.recv(): 接收conn2.send(obj)发送的对象. 如果没有消息可接收, recv()方法会一直阻塞. 如果连接的另外一端已经关闭, 那么recv()方法会抛出EOFError.
conn1.send(obj):通过连接发送对象。obj是与序列化兼容的任意对象
#其他方法:
conn1.close(): 关闭连接. 如果conn1被垃圾回收, 将自动调用此方法
conn1.fileno(): 返回连接使用的整数文件描述符
conn1.poll([timeout]): 如果连接上的数据可用, 返回True. timeout指定等待的最长时限. 如果省略此参数, 方法将立即返回结果. 如果将timeout设置成None, 操作将无限期地等待数据到达.
conn1.recv_bytes([maxlength]): 接收c.send_bytes()方法发送的一条完整的字节消息. maxlength指定要接收的最大字节数. 如果进入的消息, 超过了这个最大值, 将引发IOError异常, 并且在连接上无法进行进一步读取. 如果连接的另外一端已经关闭, 再也不存在任何数据, 将引发EOFError异常.
conn.send_bytes(buffer[,offset[,size]]): 通过连接发送字节数据缓冲区, buffer是支持缓冲区接口的任意对象, offset是缓冲区中的字节偏移量, 而size是要发送的字节数. 数据结果以单条消息的形式发出, 然后调用c.recv_bytes()函数进行接收
conn1.recv_bytes_into(buffer[,offset]): 接收一条完整的字节消息, 并把它保存在buffer对象中, 该对象支持可写入的缓冲区接口(即bytearray对象或类似的对象). offset指定缓冲区中放置消息处的字节位移. 返回值是收到的字节数. 如果消息长度大于可用的缓冲区空间, 将引发BufferTooShort异常.
示例:子进程给主进程发送消息:
from multiprocessing import Process, Pipe # 引入Pipe模块
def func(conn):
conn.send("HelloWorld!") # 子进程发送了消息
conn.close() # 子进程关闭通道的一端
if __name__ == '__main__':
parent_conn, child_conn = Pipe() # 建立管道,拿到管道的两端,双工通信方式,两端都可以收发消息
p = Process(target=func, args=(child_conn,)) # 将管道的一端给子进程
p.start() # 开启子进程
print("主进程接收>>>", parent_conn.recv()) # 主进程接收了消息
p.join()
print("主进程执行结束!")
主进程给子进程发送消息:
from multiprocessing import Process, Pipe # 引入Pipe模块
def func(conn):
msg = conn.recv() # (5)子进程通过管道的另一端接收信息
print("The massage from parent_process is>>>", msg)
if __name__ == '__main__':
parent_conn, child_conn = Pipe() # (1)创建管道,拿到管道的两端
p = Process(target=func, args=(child_conn,)) # (2)创建子进程func, 把child_conn给func
p.start() # (3)启动子进程
parent_conn.send("Hello,child_process!") # (4)主进程通过parent_conn给子进程发送信息
主进程和子进程互相收发消息:
from multiprocessing import Process, Pipe
def func(parent_conn, child_conn):
msg = parent_conn.recv() # (5)子进程使用parent_conn接收主进程的消息
print("子进程使用parent_conn接收>>>", msg) # (6)打印接收到的消息
child_conn.send("子进程使用child_conn给主进程发送了一条消息") # (7)子进程发送消息
print("子进程执行完毕")
if __name__ == '__main__':
parent_conn, child_conn = Pipe() # (1)创建管道,拿到管道两端
child_conn.send("主进程使用child_conn给子进程发送了一条消息")
p = Process(target=func, args=(parent_conn, child_conn))
p.start()
p.join()
msg = parent_conn.recv()
print("主进程使用parent_conn接收>>>", msg)
print("主进程执行完毕!")
应该特别注意管道端点的正确管理问题. 如果生产者或消费者中都没有使用管道的某个端点, 就应将它关闭,否则就会抛出异常. 例如: 当生产者关闭了管道的输出端时, 消费者也要同时关闭管道的输入端. 如果忘记执行这些步骤, 程序可能在消费者中的recv()操作上挂起(就是阻塞). 管道是由操作系统进行引用计数的, 在所有进程中关闭管道的相同一端就会生成EOFError异常. 因此, 在生产者中关闭管道不会有任何效果, 除非消费者也关闭了相同的管道端点.
管道可以用于双工通信, 通常利用在客户端/服务端中使用的请求/响应模型, 或者远程过程调用, 就可以使用管道编写与进程交互的程序。
2.6. Manager模块:进程间数据共享
homepage
展望未来, 基于消息传递的并发编程是大势所趋. 即便是使用线程, 推荐做法也是将程序设计为大量独立的线程集合, 通过消息队列交换数据. 这样极大地减少了对使用锁定和其他同步手段的需求, 还可以扩展到分布式系统中.
进程间应该尽量避免通信, 即便需要通信, 也应该选择进程安全的工具来避免加锁带来的问题, 应该尽量避免使用本节所讲的共享数据的方式, 以后我们会尝试使用数据库来解决进程之间的数据共享问题.
进程之间数据共享的模块之一Manager模块:
进程间数据是独立的, 可以借助于队列或管道实现通信, 二者都是基于消息传递的. 虽然进程间数据独立, 但可以通过Manager实现数据共享.
from multiprocessing import Process, Manager
def func(m_dic):
m_dic["name"] = "王力宏" # 修改manager字典
if __name__ == '__main__':
m = Manager() # 创建Manager对象
m_dic = m.dict({"name": "王乃卉"}) # 创建manager字典
print("主进程>>>", m_dic)
p = Process(target=func, args=(m_dic,)) # 创建子进程
p.start()
p.join()
print("主进程>>>", m_dic)
# 执行结果:
# 主进程>>> {'name': '王乃卉'}
# 主进程>>> {'name': '王力宏'}
多进程共同去处理共享数据的时候,就和我们多进程同时去操作一个文件中的数据是一样的,不加锁就会出现错误的结果,进程不安全的,所以也需要加锁。
不加锁对共享数据进行修改,是不安全的:
from multiprocessing import Process, Manager
def func(m_dic):
m_dic["count"] -= 1
if __name__ == '__main__':
m = Manager()
m_dic = m.dict({"count": 100})
p_list = []
# 开启20个进程来对共享数据进行修改
for i in range(20):
p = Process(target=func, args=(m_dic, ))
p.start()
p_list.append(p)
[p.join() for p in p_list]
print("主进程>>>", m_dic)
执行结果:偶尔会出现 “主进程>>> {'count': 81}” 的情况, 这是因为共享数据不变, 但是当多个子进程同时访问共享数据并对其进行修改时, 由于修改的过程是要重写对共享数据进行赋值的, 在这个赋值的过程中, 可能一个子进程还没来得及赋值成功, 就有另外的一个子进程拿到原先的值, 这样一来, 就会出现多个子进程修改同一个共享数据。于是就出现了上面代码结果偶尔会少减了一次的现象. 综上所述,共享数据是不够安全的, 而"加锁"是一个很好的解决办法.
from multiprocessing import Process, Manager, Lock
def func(m_dic, m_lock):
with m_lock:
m_dic["count"] -= 1
# 等同于:
# m_lock.acquire()
# m_dic["count"] -= 1
# m_lock.release()
if __name__ == '__main__':
m = Manager()
m_lock = Lock()
m_dic = m.dict({"count": 100})
p_list = []
# 开启20个进程来对共享数据进行修改
for i in range(20):
p = Process(target=func, args=(m_dic, m_lock))
p.start()
p_list.append(p)
[p.join() for p in p_list]
print("主进程", m_dic)
加锁后, 多次尝试运行程序, 执行结果稳定可靠. 不难看出, 加锁后 共享数据是安全的.
2.7. multiprocessing.Queue
homepage