python多进程处理 --- multiprocessing
GIL全局解释器锁
GIL即python全局解释器锁,这是一个存在于解释器进程中的锁,该锁的存在造成了即使是多核cpu,在同一个python进程中,只会有一个线程被调度。如果想同时使用多核的优势,就需要使用多个进程来全面利用cpu。
IO密集型和计算密集型
- IO密集型
IO密集型是指程序中有大量的IO操作,线程进行IO操作时,会进入阻塞态,阻塞态的线程不会接受CPU的线程调度,CPU的时间将分配给其他线程。即使只有一个cpu执行程序,cpu也可以轻松的调度大量IO密集的线程执行计算步骤。
- 计算密集型
计算密集型是指程序需要大量的占用CPU资源进行计算,几乎没有阻塞操作。由于GIL的存在,当线程A进行调度时,其余的线程处于阻塞状态,CPU进行线程切换时,需要等待其他线程进入就绪态。而此时线程A刚被调度完成,还没有进入阻塞,此时CPU可能又会将A线程进行调度,反复如此。导致其他线程几乎无法使用CPU。
多进程
由于CPython解释器的GIL锁的问题,使得一个CPython进程只能同时执行一个线程,使用多线程的方式运行一个计算密集型的程序并不是一个好的选择。想要提高cpu的利用率,我们就需要解决GIL的问题,既然一个CPython进程只能运行一个线程,那么使用多个Python进程就能够解决我们的问题。
Python提供了使用多线程的库multiprocessing来实现多线程运行。并提供了和多线程类似的API
import multiprocessing, time def func(): x = 0 for i in range(1000000000): # 10亿次 x = x + 1 if __name__ == "__main__": start = time.time() m1 = multiprocessing.Process(target=func) m2 = multiprocessing.Process(target=func) m1.start() m2.start() m1.join() m2.join() end = time.time() print("total_time = ", end - start) ====结果==== total_time = 65.23427520193434
如果我们使用单进程执行
import multiprocessing, time def func(): x = 0 for i in range(1000000000): # 10亿次 x = x + 1 if __name__ == "__main__": start = time.time() func() func() end = time.time() print("total_time =", end - start) =====运行结果===== total_time = 113.9023756980896
从结果可以看出使用多进程执行的时间大约为单进程执行的一半。
使用多进程虽然可以降低执行时间,但是多个进程实现数据共享代价是比较昂贵的,而在多线程中只需要使用一个全局变量即可实现。multiprocessing库提供了进程间通讯的Queue队列和Pipe管道实现数据通信。
开启进程需要耗费系统资源,这比开启一个线程的代价大得多,大量的使用多进程不是一个好的选择。可以尝试使用其他语言解决需求。并且每一个进程有一个唯一PID来标记该进程,PID是有限的系统资源,,所以在必须时候我们才使用多进程去处理任务。
进程池pool
进程池是提前开辟进程资源,将这个进程资源交由这个容器管理,当需要使用进程时,只需要向这个容器申请进程资源即可。这个容器就是mupltiprocessing.Pool对象。进程池中的进程资源可以反复利用,不需要反复的创建和销毁进程资源。
实例化一个Pool类即可得到Pool对象,它提供以下方法。
方法 | 说明 |
apply(func, args, kwds) | 阻塞执行,每次向该pool中提交一个任务,之后主进程阻塞等待任务完成,并得到func函数的返回值 |
apply_async(func, args, kwds, callback, error_callback) | 异步执行,主线程不会阻塞等待,而是把所有的任务交给pool,由pool自己调度func函数执行,pool根据自己 |
close() | 关闭进程池,池不再接受任务所有任务完成后退出 |
terminate() | 立即结束工作进程,不再处理未处理的任务 |
join() | 主进程阻塞等待子进程退出,必须在close()或者terminate()方法后使用 |
import multiprocessing, time, logging, sys def func(n): if n > 5: raise IndexError("n > 4-------------error") logging.info("func") time.sleep(3) logging.info("thi") return "return = 1000" def callback(value): logging.info("callback finished") print("value = ", value) def callback_error(value): logging.info("occur the error at") print("error-value = ", value) if __name__== "__main__": logging.basicConfig(stream=sys.stdout, level=10, format="%(msg)s: %(process)s-%(processName)s") pool = multiprocessing.Pool(3) for i in range(10): pool.apply_async(func, args=(i, ), callback=callback, error_callback=callback_error) logging.info("exit--for") pool.close() pool.join() print("------------------end")
这里定义了一个进程池可以同时处理3个进程,在for循环中提交了10个任务进行处理,主进程会将任务全部提交给pool处理,然后在pool.join()等待任务处理完毕。pool每次执行三个任务,执行完成全部任务后,主进程继续执行,结束。
在pool.apply_async方法中的参数为
参数 | 含义 |
func | 任务函数,在pool中启动新的进程来执行该func函数 |
args, kwds | 任务函数的参数,args为位置传参:元组;kwds为关键字传参:字典 |
callback(value) | 传入一个一参函数,该函数将接受func函数正常执行后的返回值作为参数。在该func任务执行完成后,由pool继续调用callback函数执行 |
error_callback(value) | 传入一个一参函数,该函数参数将接受func函数的错误作为参数。如果func函数执行发生错误,调用该函数,正常执行调用callback函数。 |
Linux几个特殊的进程
在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息,就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
孤儿进程
孤儿进程:一个父进程退出,而它的子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程(1号进程)对它们完成状态收集工作。并循环调用wait获取这些孤儿进程,所以孤儿进程没有什么危害。
僵尸进程
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,继续占用内存空间。这种进程称之为僵死进程。
守护进程
它是运行在后台的一种特殊进程,在其父进程结束后仍然正常运行并由init管理。它会周期性的执行某种任务或者等待处理某些事件。相对于普通的孤儿进程需要做一些特殊的处理。