1. 什么进程?
进程(Process),顾名思义,就是进行中的程序。有一句话说得好:程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体。进程是资源分配的最小单元,也就是说每个进程都有其单独的内存空间。
2. 如何创建一个进程?
Unix/Linux系统通过fork系统调用创建一个进程,但是在Windows中并没有fork调用。但是别担心,Python中内置的multiprocessing模块是跨平台的,我们可以通过对multiprocess模块中的Process类进行实例化创建一个进程对象,如:
import os
from multiprocessing import Process
def run_a_sub_proc(name):
print(f'子进程:{name}({os.getpid()})开始...')
if __name__ == '__main__':
print(f'主进程({os.getpid()})开始...')
# 通过对Process类进行实例化创建一个子进程
p = Process(target=run_a_sub_proc, args=('测试进程', ))
p.start()
p.join()
执行结果如下:
创建一个子进程
这里需要明确以下主进程和子进程。当我们通过python demo.py开始执行demo.py这个程序时,程序被赋予了声明,成为一个进程,这个进程是主进程。而在主进程执行过程,通过对Process类进行实例化创建的是子进程。
3. multiprocessing基本功能
3.1 进程启动
当通过对Process类实例化获得一个进程p以后,直接通过p.start()就可以启动该进程了。可是,在start()方法的背后,实际上有三种启动方法:
- spawn:子进程仅继承有限的资源,适用于Unix/Linux和Windows
- fork:子进程会继承父进程中所有的资源,仅适用于Unix/Linux
- forkserver:创建一个单进程的服务进程,专门用来处理子进程的创建,仅适用于Unix/Linux[1]
目前,对于Unix/Linux,默认的启动方法是fork;而对于Windows和MacOS系统,默认的启动方法是spawn。
3.2 join()方法
在多线程中,join()方法会使主线程进入阻塞,直到调用join()方法的子线程执行完毕。那么在多进程中,join()方法的用法是一样,即使主进程进入阻塞,直到调用join()方法的子进程执行完毕。猜猜以下两个例子的运行结果会有什么不同?
# 例一
import os, time
from multiprocessing import Process
def run_a_sub_proc(name):
print(f'子进程:{name}({os.getpid()})开始...')
for i in range(3):
print(f'子进程:{name}({os.getpid()})运行中...')
time.sleep(1)
if __name__ == '__main__':
print(f'主进程({os.getpid()})开始...')
p1 = Process(target=run_a_sub_proc, args=('进程-1', ))
p2 = Process(target=run_a_sub_proc, args=('进程-2', ))
p1.start()
p2.start()
p1.join()
p2.join()
# 例二
import os, time
from multiprocessing import Process
def run_a_sub_proc(name):
print(f'子进程:{name}({os.getpid()})开始...')
for i in range(3):
print(f'子进程:{name}({os.getpid()})运行中...')
time.sleep(1)
if __name__ == '__main__':
print(f'主进程({os.getpid()})开始...')
p1 = Process(target=run_a_sub_proc, args=('进程-1', ))
p2 = Process(target=run_a_sub_proc, args=('进程-2', ))
p1.start()
p1.join()
p2.start()
p2.join()
执行结果:
通过join方法阻塞主进程
简而言之,join()方法就是让主进程进入阻塞状态,等对应的子进程执行完毕再执行下一行,主要用于进程同步。
3.3 Pool
如果想一次性创建多个进程,可以用Pool方法(注意Pool是一个方法,不是类),如
import os, time
from multiprocessing import Process, Pool
def run_a_sub_proc(name):
print(f'子进程:{name}({os.getpid()})开始!')
for i in range(2):
print(f'子进程:{name}({os.getpid()})运行中...')
time.sleep(1)
print(f'子进程:{name}({os.getpid()})结束!')
if __name__ == '__main__':
print(f'主进程({os.getpid()})开始...')
p = Pool(3)
for i in range(1, 5):
p.apply_async(run_a_sub_proc, args=(f"进程-{i}",))
p.close()
p.join()
运行结果如下
进程1~3结束了进程4才开始
值得注意的是,在上述代码中,进程1~3结束了进程4才开始,这是为什么呢?这是因为在p=Pool(3)中定义了每次执行的子进程个数的限制。
Pool的默认大小是你所用的电脑CPU的核数,CPU核数可通过os.cpu_count()获得。
p.join()的意思是等Pool中所有的子进程全部执行完毕再进行下一步,在调用p.join()之前需要先调用p.close()。
4 进程间通信
现在设想你需要两个进程,一个进程(接收进程)产生数据(比如从网站上爬虫,或者从websocket接收数据等),另一个进程(转发进程)对产生的数据进行处理并转发(比如计算并处理之后上传数据库,或者发送给websocket等)。这是一个非常常见的应用场景,如何把接收进程接受的数据传递给转发进程呢?直接硬写是不行的,比如下面这个错误示范
import os, time, random
from multiprocessing import Process
data: int
def recv():
print(f'子进程:接收进程({os.getpid()})开始!')
while True:
global data
# 用产生随机数的方法模拟数据的接收
data = random.randint(1, 100)
print(f'子进程:接收进程接收到数据{data}!')
sleep_time = random.randint(1, 3)
time.sleep(sleep_time)
def send():
print(f'子进程:转发进程({os.getpid()})开始!')
while True:
global data
print(f'子进程:转发进程接收到数据{data}并开始处理、转发!')
if __name__ == '__main__':
print(f'主进程({os.getpid()})开始...')
p1 = Process(target=recv)
p2 = Process(target=send)
p1.start()
p2.start()
p1.join()
p2.join()
上面这个程序毫无疑问是会报错的,即便你声明了数据data是全局变量。
程序报错
报错的原因是:每个子进程享有独立的内存空间,接收进程产生的数据不能马上同步到转发进程中,这也就是为什么接收线程中提示“name ‘data’ is not defined”的原因。
那如何实现进程间通信呢?multiprocessing提供了两种方法:Queue和Pipe。
4.1 Queue
import os, time, random
from multiprocessing import Process, Queue
def recv(q):
print(f'子进程:接收进程({os.getpid()})开始!')
while True:
# 用产生随机数的方法模拟数据的接收
data = random.randint(1, 100)
print(f'子进程:接收进程接收到数据{data}!')
q.put(data)
sleep_time = random.randint(1, 3)
time.sleep(sleep_time)
def send(q):
print(f'子进程:转发进程({os.getpid()})开始!')
while True:
# 注意:如果q里面没有数据,get()方法就会等待,直到获得一个数据并赋值给data
data = q.get()
print(f'子进程:转发进程接收到数据{data}并开始处理、转发!')
time.sleep(1)
if __name__ == '__main__':
print(f'主进程({os.getpid()})开始...')
q = Queue()
p1 = Process(target=recv, args=(q,))
p2 = Process(target=send, args=(q,))
p1.start()
p2.start()
p1.join()
p2.join()
执行结果如下
通过Queue实现进程间通信
需要注意两点:
- data=q.get()过程中,如果q中没有数据,并不是返回一个None给data,get()方法会进入等待状态,直到q中有数据为止;
- queue是先进先出(FIFO)的。
4.2 Pipe
如果你创建了很多个子进程,那么其中任何一个子进程都可以对Queue进行存(put)和取(get)。但Pipe不一样,Pipe只提供两个端点,只允许两个子进程进行存(send)和取(recv)。也就是说,Pipe实现了两个子进程之间的通信。
import os, time, random
from multiprocessing import Pipe, Process
def sub_process(name, p):
print(f'子进程:{name}({os.getpid()})开始!')
while True:
data_s = random.randint(1, 100)
p.send(data_s)
print(f'子进程:{name}发送数据:{data_s}!')
data_r = p.recv()
print(f'子进程:{name}接收到数据:{data_r}!')
time.sleep(1)
if __name__ == '__main__':
print(f'主进程({os.getpid()})开始...')
conn_1, conn_2 = Pipe()
p1 = Process(target=sub_process, args=("进程-1", conn_1,))
p2 = Process(target=sub_process, args=("进程-2", conn_2,))
p1.start()
p2.start()
p1.join()
p2.join()
执行结果
通过Pipe实现进程间通信
注意:
- Queue可以被多个进程调用,而Pipe只能被两个进程调用;
- Queue是基于Pipe实现的,因此Pipe速度比Queue快很多[2]。
5.进程间数据共享
通常不鼓励进程间数据共享,因为可能会带来“竞争危害”、产生不可预知的结果。但如果有这方面的需要,在保证数据安全的基础上也是可以的。实现线程间数据共享主要有两种方法:Value/Array和Manager
5.1 Value/Array
import os, time, random
from multiprocessing import Process, Value, Array
def sub_process(name, v, arr):
print(f'子进程:{name}({os.getpid()})开始!')
while True:
if name == "修改Value":
v.value += 1 # 通过Value.value读取Value的数值
else:
num = random.randint(0, 2)
arr[num] += 1
print(f'子进程:{name}', v.value, arr[:])
time.sleep(random.randint(1, 3))
if __name__ == '__main__':
print(f'主进程({os.getpid()})开始...')
v = Value("i", 0) # i 指整数
arr = Array("i", [1, 2, 3]) # i 指整数型组成的数组
p1 = Process(target=sub_process, args=("修改Value", v, arr, ))
p2 = Process(target=sub_process, args=("修改Array", v, arr, ))
p1.start()
p2.start()
p1.join()
p2.join()
执行结果:
通过两个子线程对数值和数组不断进行修改
5.2 Manager
Manager()方法会返回一个服务进程,这个进程专门用来维护进程间数据的共享。Manager提供的数据格式非常多,包括list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array等。
6. 进程同步
在前面提到,不鼓励在进程间实现数据共享,因为容易产生竞争危害。例如两个线程,分别对同一个数值不断地进行+1,循环200遍,那么理论上最终这个数值会变成400,然而事实并非如此,如
import os, time, random
from multiprocessing import Process, Value
def sub_process(name, v):
print(f'子进程:{name}({os.getpid()})开始!')
for i in range(200):
v.value += 1
if __name__ == '__main__':
print(f'主进程({os.getpid()})开始...')
v = Value("i", 0) # i 指整数
p1 = Process(target=sub_process, args=("进程-1", v,))
p2 = Process(target=sub_process, args=("进程-2", v,))
p1.start()
p2.start()
p1.join()
p2.join()
print(v.value)
执行结果如下:数值并不是400,因为发生了竞争危害。
最终数值并不是400
解决该问题的办法就是加进程锁。
import os, time, random
from multiprocessing import Process, Value, Lock
def sub_process(name, v, lock):
print(f'子进程:{name}({os.getpid()})开始!')
for i in range(200):
lock.acquire()
v.value += 1
lock.release()
if __name__ == '__main__':
print(f'主进程({os.getpid()})开始...')
v = Value("i", 0) # i 指整数
lock = Lock()
p1 = Process(target=sub_process, args=("进程-1", v, lock))
p2 = Process(target=sub_process, args=("进程-2", v, lock))
p1.start()
p2.start()
p1.join()
p2.join()
print(v.value)
输出结果变成了预期的400:
最后
在学习python中有任何困难不懂的可以微信扫描下方CSDN官方认证二维码加入python交流学习
多多交流问题,互帮互助,这里有不错的学习教程和开发工具。
(python兼职资源+python全套学习资料)
一、Python所有方向的学习路线
Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。
二、Python必备开发工具
四、Python视频合集
观看零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
五、实战案例
光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
六、Python练习题
检查学习结果。
七、面试资料
我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
最后,千万别辜负自己当时开始的一腔热血,一起变强大变优秀。