标题
多线程与多进程概念
Python多进程编程
Process
Pool
进程间通信(Queue和Pipe)
1
多线程与多进程
在介绍Python多线程编程之前,先给大家复习一下进程和线程的概念。
进程(Process)实际上表示的就是计算机正在进行的一个任务,比如,打开一个浏览器便是启动一个浏览器进程,打开一个记事本便是启动一个记事本进程。
但是,一个进程未必只能进行一件事,就像一个Word进程,在打字的同时还会有拼写检查,这些在进程内部同时进行的多个“子任务”,就称为线程(Thread)。
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
在以往的单核CPU上,系统执行多进程的方式是通过不断的在多个进程中切换——例如任务1执行0.01秒,切到任务2执行0.01秒再切到任务3……以此类推,而在多核CPU出现后,真正的并行执行多任务才真正的得以实现,但绕是如此,一台计算机同时进行的进程是非常之多的,远远大于CPU的核心数量,因此,操作系统依然会将这些任务轮流调度到每个核心上运行。
多线程的执行方式类似于多进程,也是通过快速切换来达到看起来“同时运行”的目的
如果我们要同时进行多个任务,我们有以下三种方案:
写多个程序,然后同时运行
在一个程序中运行多个线程
多进程+多线程
2
Python多进程编程
Process
多进程的实现与你的操作系统有关。例如Unix/Linux操作系统提供了一个fork()系统调用来创建进程。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
而Python的os模块里正好封装了该系统调用,所以在Unix/Linux操作系统可以通过os.fork()创建子进程。
但是Window系统是没有这个系统调用的,因此没办法用fork()实现多进程。Python提供了一个multiprocessing模块来供跨平台版本的Python使用多进程,这个模块提供了一个Process类来代表一个进程对象。下面是一个启动子进程并等待其结束的例子:
模块os提供的方法getpid()可以让我们查看当前运行的进程的id。创建子进程时,只需要传入一个执行函数和函数的参数(注意参数args=('child_process',)后面的那个逗号,因为传参的方式是传入元祖,所以如果不加逗号,函数会认为要把'child_process'的每个字符都当作参数传入),创建一个Process实例,然后用start()方法启动。join()方法表示等待子进程结束后再继续往下运行,常用于进程间的同步。
有意思的是,如果我们传参数不按照上述代码args=()这样的形式来传参而是直接类似于target=run('a')这样的形式传入参数,就会发现,Python会直接将其作为一个函数放在主进程内运行,而不会再作为子进程单独运行。
可以看到,若是没有join()那么子进程的运行时间就不会和我们预想的同步。
Pool
如果我们要创建大量的子进程,可以利用进程池的方式来批量创建子进程。
进程池类Pool同样是由模块multiprocess导出
对于Pool对象,若要调用join()则必须提前调用close(),一旦调用close()则无法再添加新的子进程。(如果不调用close(),它会认为你还要添加子进程故无法执行join())
进程间通信
Python模块multiprcess提供Queue和Pipe类来进行进程间的通信,另外还有很多方式,这里我们先介绍提出的这两种。
Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。Queue通过put()方法把数据插入到队尾,get()方法用于从队头取出数据。并且它们都有两个参数分别为blocked和timeout。当队列已满且blocked为True的时候,如果timeout为正值,则会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常,同理当队列为空且blocked为True的时候,如果timeout为正值,则会等待timeout时间直到有数据插入再取走。若等待时间内没有型数据插入则会抛出Queue.Empty异常。(创建Queue对象时接受一个maxsize参数来限制队列里的对象个数)
以下是一个例子,我们创建两个进程,一个inputer()用于往Queue里写数据,一个reader()用于从Queue里读数据
Pipe是方法是实现两个进程通信的另一种方法。Pipe对象分两种,一种为单向管道,一种为双向管道,可以通过构造方法Pipe ( duplex = False ) 来创建单向管道(默认为双向管道)。
Pipe执行任务的方式是,一个进程从Pipe的一端输入对象,然后一个进程从Pipe的另一端接收对象,单向管道只允许管道一端的进程输入,而双向管道则允许从两端输入,下面是一个例子。
调用构造方法Pipe()创建了一个双向管道,实际上是创建了一个由两个单向管道组成的二元组,若是一个进程调用了一个单向管道的send方法,那么另外一个进程就不能再调用这个管道的send方法,我们可以从例子中看到,进程sender用了二元组第一个管道的send,进程recver用了第二个。
小结
掌握Python多进程编程技术可以充分利用多核CPU,极大的提高计算机的执行效率,例如在生成随机森林的时候,使用多进程可以提高CART的生成速率等等。下期Python基础将为大家介绍Python的多线程。
资料来源:
廖雪峰Python教程
以及网上各位大佬的博文
领取专属 10元无门槛券
私享最新 技术干货