2017-07-30 19:44:25python
什么叫“多任务”呢?简单地说,就是操做系统能够同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶做业,这就是多任务,至少同时有3个任务正在运行。还有不少任务悄悄地在后台同时运行着,只是桌面上没有显示而已。数据库
如今,多核CPU已经很是普及了,可是,即便过去的单核CPU,也能够执行多任务。因为CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?编程
答案就是操做系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每一个任务都是交替执行的,可是,因为CPU的执行速度实在是太快了,咱们感受就像全部任务都在同时执行同样。浏览器
真正的并行执行多任务只能在多核CPU上实现,可是,因为任务数量远远多于CPU的核心数量,因此,操做系统也会自动把不少任务轮流调度到每一个核心上执行。安全
对于操做系统来讲,一个任务就是一个进程(Process),好比打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。ruby
有些进程还不止同时干一件事,好比Word,它能够同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就须要同时运行多个“子任务”,咱们把进程内的这些“子任务”称为线程(Thread)。服务器
因为每一个进程至少要干一件事,因此,一个进程至少有一个线程。固然,像Word这种复杂的进程能够有多个线程,多个线程能够同时执行,多线程的执行方式和多进程是同样的,也是由操做系统在多个线程之间快速切换,让每一个线程都短暂地交替运行,看起来就像同时执行同样。固然,真正地同时执行多线程须要多核CPU才可能实现。网络
咱们前面编写的全部的Python程序,都是执行单任务的进程,也就是只有一个线程。若是咱们要同时执行多个任务怎么办?多线程
有两种解决方案:并发
一种是启动多个进程,每一个进程虽然只有一个线程,但多个进程能够一块执行多个任务。
还有一种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也能够一块执行多个任务。
固然还有第三种方法,就是启动多个进程,每一个进程再启动多个线程,这样同时执行的任务就更多了,固然这种模型更复杂,实际不多采用。
总结一下就是,多任务的实现有3种方式:
同时执行多个任务一般各个任务之间并非没有关联的,而是须要相互通讯和协调,有时,任务1必须暂停等待任务2完成后才能继续执行,有时,任务3和任务4又不能同时执行,因此,多进程和多线程的程序的复杂度要远远高于咱们前面写的单进程单线程的程序。
由于复杂度高,调试困难,因此,不是无可奈何,咱们也不想编写多任务。可是,有不少时候,没有多任务还真不行。想一想在电脑上看电影,就必须由一个线程播放视频,另外一个线程播放音频,不然,单线程实现的话就只能先把视频播放完再播放音频,或者先把音频播放完再播放视频,这显然是不行的。
Python既支持多进程,又支持多线程。
线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,彻底由操做系统决定,程序本身不能决定何时执行,执行多长时间。
多进程和多线程的程序涉及到同步、数据共享的问题,编写起来更复杂。
1、多进程
- 要实现跨平台的多进程,可使用
multiprocessing
模块中的Process类
若是你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。因为Windows没有fork
调用,难道在Windows上没法用Python编写多进程的程序?因为Python是跨平台的,天然也应该提供一个跨平台的多进程支持。multiprocessing
模块就是跨平台版本的多进程模块。multiprocessing
模块提供了一个Process
类来表明一个进程对象。
-
- 建立子进程时,只须要传入一个执行函数和函数的参数(元组类型),建立一个
Process
实例。
start()
方法启动,这样建立进程比fork()
还要简单。
join()
方法能够等待子进程结束后再继续往下运行,一般用于进程间的同步。
from multiprocessing import Process import os def runproc(name): print('This is child process-%s:%s' %(name,os.getpid())) if __name__ == '__main__': print('Parent process name:%s' %os.getpid()) p = Process(target=runproc,args=('child',)) print('Child process will start') p.start() p.join() print('Child process is ended')
# Parent process name:3748
# Child process will start
# This is child process-child:8896
# Child process is ended
- 若是要启动大量的子进程,能够用进程池的方式批量建立子进程,使用Pool类
在使用Python进行系统管理时,特别是同时操做多个文件目录或者远程控制多台主机,并行操做能够节约大量的时间。若是操做的对象数目不大时,还能够直接使用Process类动态的生成多个进程,十几个还好,可是若是上百个甚至更多,那手动去限制进程数量就显得特别的繁琐,此时进程池就派上用场了。
Pool类能够提供指定数量的进程供用户调用,当有新的请求提交到Pool中时,若是池尚未满,就会建立一个新的进程来执行请求。若是池满,请求就会告知先等待,直到池中有进程结束,才会建立新的进程来执行这些请求。
-
- Pool(n) : 表示最多同时执行多少子进程
- apply_async(func[, args=()[, kwds={}[, callback=None]]]) : 它是非阻塞且支持结果返回进行回调
- close() : 关闭进程池,使其再也不接受新的任务
- terminate() : 结束工做进程,不在处理未处理的任务
- join() : 主进程阻塞,等待子进程的完成。join()方法必须在close()方法或者terminate()方法以后使用。
- map(func, iterable[, chunksize=None]) : Pool类中的map方法,与内置的map函数用法行为基本一致,它会使进程阻塞直到返回结果。注意,虽然第二个参数是一个迭代器,但在实际使用中,必须在整个队列都就绪后,程序才会运行子进程。Map按序处理这些迭代。调用这个函数,它就会返回给咱们一个按序存储着结果的简易列表。
from multiprocessing import Pool import os, time, random def long_time_task(name): print('Run task %s (%s)...' % (name, os.getpid())) start = time.time() time.sleep(random.random() * 3) end = time.time() print('My Parent PID is: %s' %os.getppid()) print('Task %s runs %0.2f seconds.' % (name, (end - start))) if __name__=='__main__': print('Parent process PID: %s.' % os.getpid()) p = Pool(4) for i in range(5): p.apply_async(long_time_task, args=(i,)) print('time: ',time.time()) print('Waiting for all subprocesses done...') print('Parent time: ',time.time()) p.close() p.join() print('All subprocesses done.') # Parent process PID: 23444. # time: 1501498489.5833461 # time: 1501498489.5833461 # time: 1501498489.5833461 # time: 1501498489.5833461 # time: 1501498489.5833461 # Waiting for all subprocesses done... # Parent time: 1501498489.5843477 # Run task 0 (18564)... # Run task 1 (23532)... # Run task 2 (22656)... # Run task 3 (23540)... # My Parent PID is: 23444 # Task 2 runs 0.24 seconds. # Run task 4 (22656)... # My Parent PID is: 23444 # Task 4 runs 0.50 seconds. # My Parent PID is: 23444 # Task 1 runs 1.26 seconds. # My Parent PID is: 23444 # Task 0 runs 2.30 seconds. # My Parent PID is: 23444 # Task 3 runs 2.60 seconds. # All subprocesses done.
一个进程建立子进程以后,进程与产生的进程之间的关系是父子关系,分别成为进程和子进程。子进程一经产生就与你进程并发执行,子进程共享父进程和子进程。子进程一经产生就与你进程并发执行,子进程共享父进程的正文段和已经打开的文件。父进程和子进程的前后顺序由系统调度。这也就是为何先打印for循环外面的量的缘由。Pool(n)是生成一个至多n个子进程并发的进程池,若是这里改为3的话,那么只会容许前三个task 先执行,执行完了才执行后面的。
import os import time from multiprocessing import Pool def getFile(path) : #获取目录下的文件list fileList = [] for root, dirs, files in list(os.walk(path)) : for i in files : if i.endswith('.txt') or i.endswith('.10w') : fileList.append(root + "\\" + i) return fileList def operFile(filePath) : #统计每一个文件中行数和字符数,并返回 filePath = filePath fp = open(filePath) content = fp.readlines() fp.close() lines = len(content) alphaNum = 0 for i in content : alphaNum += len(i.strip('\n')) return lines,alphaNum,filePath def out(list1, writeFilePath) : #将统计结果写入结果文件中 fileLines = 0 charNum = 0 fp = open(writeFilePath,'a') for i in list1 : fp.write(i[2] + " 行数:"+ str(i[0]) + " 字符数:"+str(i[1]) + "\n") fileLines += i[0] charNum += i[1] fp.close() print fileLines, charNum if __name__ == "__main__": #建立多个进程去统计目录中全部文件的行数和字符数 startTime = time.time() filePath = "C:\\wcx\\a" fileList = getFile(filePath) pool = Pool(5) resultList =pool.map(operFile, fileList) pool.close() pool.join() writeFilePath = "c:\\wcx\\res.txt" print resultList out(resultList, writeFilePath) endTime = time.time() print "used time is ", endTime - startTime
Process
之间确定是须要通讯的,操做系统提供了不少机制来实现进程间的通讯。Python的multiprocessing
模块包装了底层的机制,提供了Queue
、Pipes
等多种方式来交换数据。
这里以queue为例。
from multiprocessing import Process, Queue import os, time, random # 写数据进程执行的代码: def write(q): print('Process to write: %s' % os.getpid()) for value in ['A', 'B', 'C']: print('Put %s to queue...' % value) q.put(value) time.sleep(random.random()) # 读数据进程执行的代码: def read(q): print('Process to read: %s' % os.getpid()) while True: value = q.get(True) print('Get %s from queue.' % value) if __name__=='__main__': # 父进程建立Queue,并传给各个子进程: q = Queue() pw = Process(target=write, args=(q,)) pr = Process(target=read, args=(q,)) # 启动子进程pw,写入: pw.start() # 启动子进程pr,读取: pr.start() # 等待pw结束: pw.join() # pr进程里是死循环,没法等待其结束,只能强行终止: pr.terminate()
# 结果输出为:
'''
Process to write: 50563 Put A to queue... Process to read: 50564 Get A from queue. Put B to queue... Get B from queue. Put C to queue... Get C from queue.
'''
2、多线程
多任务能够由多进程完成,也能够由一个进程内的多线程完成。咱们前面提到了进程是由若干线程组成的,一个进程至少有一个线程。
因为线程是操做系统直接支持的执行单元,所以,高级语言一般都内置多线程的支持,Python也不例外,而且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:_thread
和threading
,_thread
是低级模块,threading
是高级模块,对_thread
进行了封装。绝大多数状况下,咱们只须要使用threading
这个高级模块。
启动一个线程就是把一个函数传入并建立Thread
实例,而后调用start()
开始执行。
多线程相似于同时执行多个不一样程序,多线程运行有以下优势:
- 使用线程能够把占据长时间的程序中的任务放到后台去处理。
- 用户界面能够更加吸引人,这样好比用户点击了一个按钮去触发某些事件的处理,能够弹出一个进度条来显示处理的进度
- 程序的运行速度可能加快
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种状况下咱们能够释放一些珍贵的资源如内存占用等等。
线程在执行过程当中与进程仍是有区别的。每一个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。可是线程不可以独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
每一个线程都有他本身的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程老是在进程获得上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
- 线程能够被抢占(中断)。
- 在其余线程正在运行时,线程能够暂时搁置(也称为睡眠) -- 这就是线程的退让。
线程能够分为:
- 内核线程:由操做系统内核建立和撤销。
- 用户线程:不须要内核支持而在用户程序中实现的线程。
Python3 线程中经常使用的两个模块为:
thread 模块已被废弃。用户可使用 threading 模块代替。因此,在 Python3 中不能再使用"thread" 模块。为了兼容性,Python3 将 thread 重命名为 "_thread"。
构造方法:
Thread(group=None, target=None, name=None, args=(), kwargs={})
- group: 线程组,目前尚未实现,库引用中提示必须是None;
- target: 要执行的方法;
- name: 线程名;
- args/kwargs: 要传入方法的参数。
Thread类提供了如下方法:
run(): 用以表示线程活动的方法。
start():启动线程活动。
join([time]): 等待至线程停止。这阻塞调用线程直至线程的join() 方法被调用停止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
isAlive(): 返回线程是否活动的。
getName(): 返回线程名。
setName(): 设置线程名。
Threading 模块提供的经常使用方法:
threading.currentThread(): 返回当前的线程变量。
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
import time, threading # 新线程执行的代码: def loop(): print('thread %s is running...' % threading.current_thread().name) n = 0 while n < 5: n = n + 1 print('thread %s >>> %s' % (threading.current_thread().name, n)) time.sleep(1) print('thread %s ended.' % threading.current_thread().name) print('thread %s is running...' % threading.current_thread().name) t = threading.Thread(target=loop, name='LoopThread') t.start() t.join() print('thread %s ended.' % threading.current_thread().name) # thread MainThread is running... # thread LoopThread is running... # thread LoopThread >>> 1 # thread LoopThread >>> 2 # thread LoopThread >>> 3 # thread LoopThread >>> 4 # thread LoopThread >>> 5 # thread LoopThread ended. # thread MainThread ended.
因为任何进程默认就会启动一个线程,咱们把该线程称为主线程,主线程又能够启动新的线程,Python的threading
模块有个current_thread()
函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread
,子线程的名字在建立时指定,咱们用LoopThread
命名子线程。名字仅仅在打印时用来显示,彻底没有其余意义,若是不起名字Python就自动给线程命名为Thread-1
,Thread-2
……
3、线程同步以及线程优先级队列
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要当心死锁的发生。
Python解释器因为设计时有GIL全局锁,致使了多线程没法利用多核。多线程的并发在Python中就是一个美丽的梦。
多线程和多进程最大的不一样在于,多进程中,同一个变量,各自有一份拷贝存在于每一个进程中,互不影响,而多线程中,全部变量都由全部线程共享,因此,任何一个变量均可以被任何一个线程修改,所以,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
为了不这种状况,引入了锁的概念。
锁有两种状态——锁定和未锁定。每当一个线程好比"set"要访问共享数据时,必须先得到锁定;若是已经有别的线程好比"print"得到锁定了,那么就让线程"set"暂停,也就是同步阻塞;等到线程"print"访问完毕,释放锁之后,再让线程"set"继续。
import time, threading # 假定这是你的银行存款: balance = 0 lock = threading.Lock() def change_it(n): # 先存后取,结果应该为0: global balance balance = balance + n balance = balance - n def run_thread(n): for i in range(100000): # 先要获取锁: lock.acquire() try: # 放心地改吧: change_it(n) finally: # 改完了必定要释放锁: lock.release()
当多个线程同时执行lock.acquire()
时,只有一个线程能成功地获取锁,而后继续执行代码,其余线程就继续等待直到得到锁为止。
得到锁的线程用完后必定要释放锁,不然那些苦苦等待锁的线程将永远等待下去,成为死线程。因此咱们用try...finally
来确保锁必定会被释放。
锁的好处就是确保了某段关键代码只能由一个线程从头至尾完整地执行,坏处固然也不少,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地降低了。其次,因为能够存在多个锁,不一样的线程持有不一样的锁,并试图获取对方持有的锁时,可能会形成死锁,致使多个线程所有挂起,既不能执行,也没法结束,只能靠操做系统强制终止。
启动与CPU核心数量相同的N个线程,在4核CPU上能够监控到CPU占用率仅有102%,也就是仅使用了一核。
可是用C、C++或Java来改写相同的死循环,直接能够把所有核心跑满,4核就跑到400%,8核就跑到800%,为何Python不行呢?
由于Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先得到GIL锁,而后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把全部线程的执行代码都给上了锁,因此,多线程在Python中只能交替执行,即便100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,一般咱们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
因此,在Python中,可使用多线程,但不要期望能有效利用多核。若是必定要经过多线程利用多核,那只能经过C扩展来实现,不过这样就失去了Python简单易用的特色。
不过,也不用过于担忧,Python虽然不能利用多线程实现多核任务,但能够经过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue。
这些队列都实现了锁原语,可以在多线程中直接使用,可使用队列来实现线程间的同步。
Queue 模块中的经常使用方法:
- Queue.qsize() 返回队列的大小
- Queue.empty() 若是队列为空,返回True,反之False
- Queue.full() 若是队列满了,返回True,反之False
- Queue.full 与 maxsize 大小对应
- Queue.get([block[, timeout]])获取队列,timeout等待时间
- Queue.get_nowait() 至关Queue.get(False)
- Queue.put(item) 写入队列,timeout等待时间
- Queue.put_nowait(item) 至关Queue.put(item, False)
- Queue.task_done() 在完成一项工做以后,Queue.task_done()函数向任务已经完成的队列发送一个信号
- Queue.join() 实际上意味着等到队列为空,再执行别的操做
import queue
import threading
import time
exitFlag = 0
class myThread (threading.Thread):
def __init__(self, threadID, name, q):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.q = q
def run(self):
print ("开启线程:" + self.name)
process_data(self.name, self.q)
print ("退出线程:" + self.name)
def process_data(threadName, q):
while not exitFlag:
queueLock.acquire()
if not workQueue.empty():
data = q.get()
queueLock.release()
print ("%s processing %s" % (threadName, data))
else:
queueLock.release()
time.sleep(1)
threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = queue.Queue(10)
threads = []
threadID = 1
# 建立新线程
for tName in threadList:
thread = myThread(threadID, tName, workQueue)
thread.start()
threads.append(thread)
threadID += 1
# 填充队列
queueLock.acquire()
for word in nameList:
workQueue.put(word)
queueLock.release()
# 等待队列清空
while not workQueue.empty():
pass
# 通知线程是时候退出
exitFlag = 1
# 等待全部线程完成
for t in threads:
t.join()
print ("退出主线程")
# 开启线程:Thread-1
# 开启线程:Thread-2
# 开启线程:Thread-3
# Thread-2 processing One
# Thread-3 processing Two
# Thread-1 processing Three
# Thread-1 processing Four
# Thread-2 processing Five
# 退出线程:Thread-1
# 退出线程:Thread-2
# 退出线程:Thread-3
# 退出主线程
4、线程的局部变量
在多线程环境下,每一个线程都有本身的数据。一个线程使用本身的局部变量比使用全局变量好,由于局部变量只有线程本身能看见,不会影响其余线程,而全局变量的修改必须加锁。
可是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦:
def process_student(name):
std = Student(name)
# std是局部变量,可是每一个函数都要用它,所以必须传进去:
do_task_1(std)
do_task_2(std)
def do_task_1(std):
do_subtask_1(std)
do_subtask_2(std)
def do_task_2(std):
do_subtask_2(std)
do_subtask_2(std)
每一个函数一层一层调用都这么传参数那还得了?用全局变量?也不行,由于每一个线程处理不一样的Student
对象,不能共享。
若是用一个全局dict
存放全部的Student
对象,而后以thread
自身做为key
得到线程对应的Student
对象如何?
global_dict = {}
def std_thread(name):
std = Student(name)
# 把std放到全局变量global_dict中:
global_dict[threading.current_thread()] = std
do_task_1()
do_task_2()
def do_task_1():
# 不传入std,而是根据当前线程查找:
std = global_dict[threading.current_thread()]
...
def do_task_2():
# 任何函数均可以查找出当前线程的std变量:
std = global_dict[threading.current_thread()]
...
这种方式理论上是可行的,它最大的优势是消除了std
对象在每层函数中的传递问题,可是,每一个函数获取std
的代码有点丑。
有没有更简单的方式?
ThreadLocal
应运而生,不用查找dict
,ThreadLocal
帮你自动作这件事:
import threading
# 建立全局ThreadLocal对象:
local_school = threading.local()
def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()
t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
# Hello, Alice (in Thread-A) # Hello, Bob (in Thread-B)
全局变量local_school
就是一个ThreadLocal
对象,每一个Thread
对它均可以读写student
属性,但互不影响。你能够把local_school
当作全局变量,但每一个属性如local_school.student
都是线程的局部变量,能够任意读写而互不干扰,也不用管理锁的问题,ThreadLocal
内部会处理。
能够理解为全局变量local_school
是一个dict
,不但能够用local_school.student
,还能够绑定其余变量,如local_school.teacher
等等。
ThreadLocal
最经常使用的地方就是为每一个线程绑定一个数据库链接,HTTP请求,用户身份信息等,这样一个线程的全部调用到的处理函数均可以很是方便地访问这些资源。
一个ThreadLocal
变量虽然是全局变量,但每一个线程都只能读写本身线程的独立副本,互不干扰。ThreadLocal
解决了参数在一个线程中各个函数之间互相传递的问题。
5、进程与线程的比较
咱们介绍了多进程和多线程,这是实现多任务最经常使用的两种方式。如今,咱们来讨论一下这两种方式的优缺点。
首先,要实现多任务,一般咱们会设计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。为了缓解这个问题,IIS和Apache如今又有多进程+多线程的混合模式,真是把问题越搞越复杂。
不管是多进程仍是多线程,只要数量一多,效率确定上不去,为何呢?
咱们打个比方,假设你不幸正在准备中考,天天晚上须要作语文、数学、英语、物理、化学这5科的做业,每项做业耗时1小时。
若是你先花1小时作语文做业,作完了,再花1小时作数学做业,这样,依次所有作完,一共花5小时,这种方式称为单任务模型,或者批处理任务模型。
假设你打算切换到多任务模型,能够先作1分钟语文,再切换到数学做业,作1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是同样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科做业。
可是,切换做业是有代价的,好比从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),而后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始作数学做业。操做系统在切换进程或者线程时也是同样的,它须要先保存当前执行的现场环境(CPU寄存器状态、内存页等),而后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,可是也须要耗费时间。若是有几千个任务同时进行,操做系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种状况最多见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
因此,多任务一旦多到一个限度,就会消耗掉系统全部的资源,结果效率急剧降低,全部任务都作很差。
是否采用多任务的第二个考虑是任务的类型。咱们能够把任务分为计算密集型和IO密集型。
计算密集型任务的特色是要进行大量的计算,消耗CPU资源,好比计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也能够用多任务完成,可是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,因此,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务因为主要消耗CPU资源,所以,代码运行效率相当重要。Python这样的脚本语言运行效率很低,彻底不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特色是CPU消耗不多,任务的大部分时间都在等待IO操做完成(由于IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,好比Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间不多,所以,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,彻底没法提高运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
考虑到CPU和IO之间巨大的速度差别,一个任务在执行的过程当中大部分时间都在等待IO操做,单进程单线程模型会致使别的任务没法并行执行,所以,咱们才须要多进程模型或者多线程模型来支持多任务并发执行。
现代操做系统对IO操做已经作了巨大的改进,最大的特色就是支持异步IO。若是充分利用操做系统提供的异步IO支持,就能够用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就能够高效地支持多任务。在多核CPU上,能够运行多个进程(数量与CPU核心数相同),充分利用多核CPU。因为系统总的进程数量十分有限,所以操做系统调度很是高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单线程的异步编程模型称为协程,有了协程的支持,就能够基于事件驱动编写高效的多任务程序。咱们会在后面讨论如何编写协程。
6、分布式进程
在Thread和Process中,应当优选Process,由于Process更稳定,并且,Process能够分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing
模块不但支持多进程,其中managers
子模块还支持把多进程分布到多台机器上。一个服务进程能够做为调度者,将任务分布到其余多个进程中,依靠网络通讯。因为managers
模块封装很好,没必要了解网络通讯的细节,就能够很容易地编写分布式多进程程序。
举个例子:若是咱们已经有一个经过Queue
通讯的多进程程序在同一台机器上运行,如今,因为处理任务的进程任务繁重,但愿把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现?
原有的Queue
能够继续使用,可是,经过managers
模块把Queue
经过网络暴露出去,就可让其余机器的进程访问Queue
了。
本文借鉴廖雪峰老师的官方文档进行整改,特此说明。