多任务概念
多任务是指用户在用一时间运行多个应用程序,每个应用程序被称作一个任务。
当多任务操作系统使用某种任务调度策略允许两个或更多进程并发共享一个处理器时,事实上处理器在某一时刻只会给一件任务提供服务。因为任务调度机制保证不同任务之间的切换速度十分迅速,因此给人多个任务同时运行的错觉。多任务系统中有3个功能单位:任务、进程和线程。
简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
多任务的实现有3种方式:
进程的概念
计算机程序只是存储在磁盘上的可执行二进制(或者其他类型)文件。只有把它们加载到内存中并被操作系统调用,才能拥有生命周期。进程则是一个执行中的程序。每个进程都拥有自己的地址空间,内存,数据栈以及其他用于跟踪执行的辅助数据。操作系统管理其上所有进程的执行,并为这些进程合理地分配时间。进程也可以通过派生(fork或spawn)新的进程来执行其他任务,不过因为每个新进程也都拥有自己的内存和数据栈等,所以只能采用进程间通信(IPC)的方式共享信息。
于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
父进程一定先执行, 一旦启动子进程,后续的代码就并发,没有先后顺序
Linux 操作系统提供了一个 fork() 函数用来创建子进程,这个函数很特殊,调用一次,返回两次,因为操作系统是将当前的进程(父进程)复制了一份(子进程),然后分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的 PID。我们可以通过判断返回值是不是 0 来判断当前是在父进程还是子进程中执行。
在 Python 中同样提供了 fork() 函数,此函数位于 os 模块下。
子进程是父进程通过fork()产生出来的,pid = os.fork()
通过返回值pid是否为0,判断是否为子进程,如果是0,则表示是子进程
由于 fork() 是 Linux 上的概念,所以如果要跨平台,最好还是使用 subprocess 模块来创建子进程。
python中采用os.wait()方法用来回收子进程占用的资源
pid, result = os.wait() # 回收子进程资源 阻塞,等待子进程执行完成回收
如果有子进程没有被回收的,但是父进程已经死掉了,这个子进程就是僵尸进程。
一个父进程已经死亡,然而他的子进程还在执行,这时候操作系统会接管这些孤儿进程。
进程创建
python的进程multiprocessing模块有多种创建进程的方式,每种创建方式和进程资源的回收都不太相同,下面分别针对系统自带的fork,Process以及Pool三种进程分析。
Fork
Linux下使用fork函数:
一个fork()属于系统调用,它比较特殊。调用一次,返回两次,因为操作系统自动把当前进程和子进程进行返回。子进程永远返回0,而父进程返回子进程的ID。
multiprocess模块
Process语法结构如下:
Process([group [, target [, name [, args [,kwargs]]]]])
Process类常用方法:
Process类常用属性:
python的多进程编程主要依靠multiprocess模块。我们先对比两段代码,看看多进程编程的优势。我们模拟了一个非常耗时的任务,计算8的20次方,为了使这个任务显得更耗时,我们还让它sleep 2秒。第一段代码是单进程计算(代码如下所示),我们按顺序执行代码,重复计算2次,并打印出总共耗时。
第2段代码是多进程计算代码。我们利用multiprocess模块的
Process方法创建了两个新的进程p1和p2来进行并行计算。Process方法接收两个参数, 第一个是target,一般指向函数名,第二个时args,需要向函数传递的参数。对于创建的新进程,调用start()方法即可让其开始。我们可以使用os.getpid()打印出当前进程的名字
知识点:
进程池讲解与应用
很多时候系统都需要创建多个进程以提高CPU的利用率,当数量较少时,可以手动生成一个个Process实例。当进程数量很多时,或许可以利用循环,但是这需要程序员手动管理系统中并发进程的数量,有时会很麻烦。这时进程池Pool就可以发挥其功效了。可以通过传递参数限制并发进程的数量,默认值为CPU的核数。
初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求,但是如果池中的进程数已经到达指定的最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来执行。
进程池:创建指定数量的进程供用户调用
对应类:from multiprocessing import Pool
常用的方法:
方法 |
说明 |
Pools= Pool(processes=None) |
创建进程池对象 |
Pools.apply(func,args=(),kwds={}) |
添加任务,阻塞模式,添加一个任务执行任务,如果一个任务不结束另一个任务就进不来 |
Pools.apply_async(func,args=(),kwds={},callback=None,error_callnack=None) |
非阻塞模式,其中callback(回调函数)等待任务完成之后才去调用 |
Pools.close() |
关闭进程池 |
Pools.join() |
等等所有任务结束 |
1.apply_async
函数原型:apply_async(func[, args=()[, kwds={}[, callback=None]]])
全部添加到队列中,立刻返回,并没有等待其他的进程执行完毕
其作用是向进程池提交需要执行的函数及参数, 各个进程采用非阻塞(异步)的调用方式,即每个子进程只管运行自己的,不管其它进程是否已经完成。这是默认方式。
2.close()
关闭进程池(pool),使其不在接受新的任务。
3. terminate()
结束工作进程,不在处理未处理的任务。
4.join()
主进程阻塞等待子进程的退出, join方法要在close或terminate之后使用。
下面的列子时为了说了进程池是依赖主进程存在而存在的,主进程死了,进程池也会挂
下例是一个简单的multiprocessing.Pool类的实例。开启了一个容量为4的进程池。4个进程需要计算5次,你可以想象4个进程并行4次计算任务后,还剩一次计算任务(任务4)没有完成,系统会等待4个进程完成后重新安排一个进程来计算。
知识点:
进程间的通信
通常,进程之间是相互独立的,每个进程都有独立的内存。通过共享内存(nmap模块),进程之间可以共享对象,使多个进程可以访问同一个变量(地址相同,变量名可能不同)。多进程共享资源必然会导致进程间相互竞争,所以应该尽最大可能防止使用共享状态。还有一种方式就是使用队列queue来实现不同进程间的通信或数据共享。
进程之间的通信方式:
消息队列:from multiprocessing import Queue
共享内存:from multiprocessing import Value,Array
消息队列
方法 |
说明 |
Msqg = Queue(maxsize=0) |
创建消息队列 |
Msgq.put(obj,block=True,timeout=None) |
消息入队,如果队列满了,消息只能等待,timeout表示等待时间 |
Msgq.get(block=True,timeout=None) |
消息出队 |
gq.qsize() |
获取消息队列的数量 |
下面这段代码中创建2个独立进程,一个负责下载,一个负责保存,实现了共享一个队列queue。
下例这段代码中创建了2个独立进程,一个负责写(pw), 一个负责读(pr), 实现了共享一个队列queue。