赞
踩
对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些 '子任务' 称为线程(Thread)。
由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。
我们前面编写的所有的Python程序,都是执行单任务的进程,也就是只有一个线程。如果我们要同时执行多个任务怎么办?
多任务的实现有3种方式:
- 多进程模式:启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。
- 多线程模式:启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。
- 多进程+多线程模式:启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,但这种模型很复杂,实际很少采用。
当然,“多任务' 在Mac OS X,UNIX,Linux,Windows等操作系统上都可以实现,本篇文章我们主要讲述如何在Windows操作系统上用Python程序实现多个任务。
由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。multiprocessing
模块就是跨平台版本的多进程模块。
multiprocessing
模块提供了一个Process
类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:- from multiprocessing import Process #multiprocessing模块提供了一个Process类来代表一个进程对象
- import requests
- import re
- import time
- code=['002348','002426','300173','000030','300542']
- m1=re.compile(r"price: '(\d{1,3}\.\d{2})'") #预编译匹配股票的价格
-
- #子进程要执行的代码
- def getprice(id):
- txt=requests.get("http://quotes.money.163.com/1%s.html"%id).text
- price=m1.findall(txt)[0] #获取股票价格
- print(id,price)
-
- if __name__=='__main__':
- #创建子进程时,只需要传入一个执行函数和函数的参数,就可以创建一个Process实例,
- print('Child process will start.')
- start=time.time()
- for id in code:
- p = Process(target=getprice, args=(id,)) #注意,如果只传入一个参数,需用逗号隔开
- p.start() #start()方法用于启动进程
- p.join() #join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
- print('Child process end.')
- print('共耗时:',time.time()-start)
执行结果:
- Child process will start.
- 300173 5.49
- 002348 4.12
- 002426 3.45
- 000030 4.68
- 300542 15.49
- Child process end.
- 共耗时: 0.93099045753479
- #如果要启动大量的子进程,可以用进程池(Pool)的方式批量创建子进程
- from multiprocessing import Pool #跨平台版本的多进程模块
- import requests
- import re
- import time
- code=['002348','002426','300173','000030','300542']
- m1=re.compile(r"price: '(\d{1,3}\.\d{2})'") #预编译匹配股票的价格
-
- #子进程要执行的代码
- def getprice(id):
- txt=requests.get("http://quotes.money.163.com/1%s.html"%id).text
- price=m1.findall(txt)[0] #获取股票价格
- print(id,price)
-
- if __name__=="__main__":
- start=time.time()
- #创建多进程
- p=Pool(6) #可以同时跑6个进程;Pool的默认大小是CPU的核数,
- for id in code:
- p.apply_async(getprice,args=(id,))
- print("Child process will start")
- #对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。
- p.close()
- p.join()
- print("Child process end")
- print('共耗时:',time.time()-start)
执行结果如下:
- Waiting for all subprocesses done...
- 002348 4.12
- 002426 3.45
- 300173 5.49
- 000030 4.68
- 300542 15.49
- All subprocesses done.
- 共耗时: 1.1254031658172607
多任务可以由多进程完成,也可以由一个进程内的多线程完成。由于进程是由若干线程组成的,一个进程至少有一个线程。
由于线程是操作系统直接支持的执行单元,因此,Python内置了多线程的支持。
Python的标准库提供了两个模块:Thread
和 threading
。Thread
是低级模块;threading
是高级模块,对 Thread
进行了封装。
- #多线程
- import time
- import re
- import requests
- from threading import Thread #导入模块
-
- code=['002348','002426','300173','000030','300542']
- m1=re.compile(r"price: '(\d{1,3}\.\d{2})'") #预编译,匹配股票价格
-
- #把要执行的代码写到getprice函数里面,线程在创建后会直接运行getprice函数
- def getprice(id):
- txt=requests.get("http://quotes.money.163.com/1%s.html"%id).text
- price=m1.findall(txt)[0] #获取股票价格
- print(id,price)
-
- if __name__=="__main__":
- #创建多线程,任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程
- ts=[]
- start=time.time()
- for id in code:
- t = Thread(target=getprice,args=(id,)) #创建线程;(多线程类似于银行叫号办理业务,那个窗口办理完业务,就叫下一个人,继续办理业务)
- ts.append(t)
- t.start() #开启线程
- for t in ts: #遍历列表ts
- t.join()
- print('共耗时:',time.time()-start)
执行结果如下:
- 300173 5.49
- 300542 15.49
- 002348 4.12
- 002426 3.45
- 000030 4.68
- 共耗时: 0.22029542922973633
- #多线程 作者:成纤纤
- import time
- import re
- import requests
- import threading
-
- code=['002348','002426','300173','000030','300542']
- m1=re.compile(r"price: '(\d{1,3}\.\d{2})'")
- def getprice(id):
- print('thread %s is running...' % threading.current_thread().name)
- txt=requests.get("http://quotes.money.163.com/1%s.html"%id).text
- price=m1.findall(txt)[0] #获取股票价格
- print(id,price)
- print('thread %s ended.' % threading.current_thread().name)
-
- if __name__=="__main__":
- #创建多线程,任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程
- ts=[]
- start=time.time()
- print('thread %s is running...' % threading.current_thread().name)
- for id in code:
- t = threading.Thread(target=getprice,args=(id,))
- ts.append(t)
- t.start()
- for t in ts:
- t.join()
- print('thread %s ended.' % threading.current_thread().name)
- print('共耗时:',time.time()-start)
执行结果如下:
- thread MainThread is running...
- thread Thread-1 is running...
- thread Thread-2 is running...
- thread Thread-3 is running...
- thread Thread-4 is running...
- thread Thread-5 is running...
- 300542 15.49
- thread Thread-5 ended.
- 300173 5.49
- thread Thread-3 ended.
- 002348 4.12
- thread Thread-1 ended.
- 002426 3.45
- thread Thread-2 ended.
- 000030 4.68
- thread Thread-4 ended.
- thread MainThread ended.
- 共耗时: 0.545579195022583
由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的
threading
模块有个current_thread()
函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread
,子线程的名字在创建时指定,我们用LoopThread
命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1
,Thread-2 ......
首先,要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。
如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用
fork
调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:'该程序执行了非法操作,即将关闭',其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。
无论是多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢?
打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。
如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时,这种方式称为单任务模型,或者批处理任务模型。
假设你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是一样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科作业。
但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。