多任务:进程、线程、协程总结及关系

多线程:python


1. 对线程的理解缓存

1.一个程序运行起来至少有一个进程,一个进程至少有一个线程
2.处理器cpu分配给线程,即cpu真正运行的是线程中的代码
3.分配cpu给线程时,是经过时间片轮训方式进行的
4.进程是操做系统分配程序执行资源的单位,而线程是进程的一个实体,
 是CPU调度和分配的单位。安全

 

2. python实现多线程的两种方式
python的thread模块是比较底层的模块,python的threading模块是对thread作了一些包装的,能够更加方便的被使用,经过threading模块能够建立线程,通常咱们都使用threading网络

 

3. 线程什么时候开启,什么时候结束
1.子线程什么时候开启,什么时候运行
   当调用thread.start()时 开启线程,再运行线程的代码
2.子线程什么时候结束
   子线程把target指向的函数中的语句执行完毕后,或者线程中的run函数代码执行完毕后,当即结束当前子线程
3.查看当前线程数量
   经过threading.enumerate()可枚举当前运行的全部线程
4.主线程什么时候结束
   全部子线程执行完毕后,主线程才结束多线程

 

4.多线程的建立与执行都是无序的,同一个进程里面的多线程共享全局变量,全部对于多个线程间共享数据很方便,执行效率也就比多进程更高;但缺点就是容易形成多线程对全局变量的随意遂改,就可能致使全局变量的混乱(即线程是非安全的),还有若是多个线程同时对一个全局变量操做,还会出现资源竞争问题,从而致使数据结果不正确,即会遇到线程安全问题。对于线程的安全问题,咱们会使用同步机制解决,同步就是协同步调,按预约的前后次序进行运行(这里的同步实质上是咱们生活上的异步)。咱们最经常使用的同步机制就是使用互斥锁,互斥锁为资源引入一个状态:锁定/非锁定;某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其余线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其余的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操做,从而保证了多线程状况下数据的正确性。并发

 

上锁解锁过程:
当一个线程调用锁的acquire()方法得到锁时,锁就进入“locked”状态。
每次只有一个线程能够得到锁。若是此时另外一个线程试图得到这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁以后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来得到锁,并使得该线程进入运行(running)状态。app

 

多进程:dom


程序:好比电脑安装了不少程序,又好比咱们编写一个xxx.py程序,它们静静的保存在硬盘中,因此程序是一个静态的概念
进程:一个程序运行起来后,代码+用到的资源 称之为进程,它是操做系统分配资源的基本单位。异步

 1.进程的状态
工做中,任务数每每大于cpu的核数,即必定有一些任务正在执行,而另一些任务在等待cpu进行执行,所以致使了有了不一样的状态async

 • 就绪态:运行的条件都已经知足,正在等在cpu执行
 • 执行态:cpu正在执行其功能
 • 等待态:等待某些条件知足,例如一个程序sleep了,此时就处于等待态,红绿灯,等待消息回复,等待同步锁 等都是处于等待态

2.进程的建立-multiprocessing
multiprocessing模块就是跨平台版本的多进程模块,提供了一个Process类来建立进程对象,进程之间不共享全局变量

3. Process语法结构以下:
Process([group [, target [, name [, args [, kwargs]]]]])
• target:若是传递了函数的引用,能够认为这个子进程就执行这里的代码
• args:给target指定的函数传递的参数,以元组的方式传递
• kwargs:给target指定的函数传递命名参数
• name:给进程设定一个名字,能够不设定
• group:指定进程组,大多数状况下用不到
Process建立的实例对象的经常使用方法:
• start():启动子进程实例(建立子进程)
• is_alive():判断进程子进程是否还在活着
• join([timeout]):是否等待子进程执行结束,或等待多少秒
• terminate():无论任务是否完成,当即终止子进程
Process建立的实例对象的经常使用属性:
• name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
• pid:当前进程的pid(进程号)

 

进程、线程对比:


功能
• 进程,可以完成多任务,好比运行的QQ再单独开一个进程接收推送的消息
• 线程,可以完成多任务,好比运行的QQ开多个线程来发送消息、接收文件、视频聊天等多个任务
定义的不一样
• 进程是操做系统进行资源分配和调度的一个基本单位.
• 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程本身基本上不拥有系统资源,可是它可与同属一个进程的其余的线程共享进程所拥有的所有资源.


区别
• 一个程序至少有一个进程,一个进程至少有一个线程.
• 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
• 进程在执行过程当中拥有独立的内存单元,而多个线程共享内存,从而极大地提升了程序的运行效率
• 线程不可以独立执行,必须依存在进程中
• 能够将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人

多进程适合在 CPU 密集型操做(cpu 操做指令比较多,如位数多的浮点运算)。
多线程适合在 IO 密集型操做(读写数据操做较多的,好比爬虫)

 

优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。

 

进程间通讯-Queue
Process之间有时须要通讯,操做系统提供了不少机制来实现进程间的通讯。


1. Queue的使用
可使用multiprocessing模块的Queue实现多进程之间的数据传递,Queue自己是一个消息列队程序,首先用一个小实例来演示一下Queue的工做原理:

#coding=utf-8
from multiprocessing import Queue q=Queue(3) #初始化一个Queue对象,最多可接收三条put消息
q.put("消息1") q.put("消息2") print(q.full())  #False
q.put("消息3") print(q.full()) #True

#由于消息列队已满下面的try都会抛出异常,第一个try会等待2秒后再抛出异常,第二个Try会马上抛出异常
try: q.put("消息4",True,2) except: print("消息列队已满,现有消息数量:%s"%q.qsize()) try: q.put_nowait("消息4") except: print("消息列队已满,现有消息数量:%s"%q.qsize()) #推荐的方式,先判断消息列队是否已满,再写入
if not q.full(): q.put_nowait("消息4") #读取消息时,先判断消息列队是否为空,再读取
if not q.empty(): for i in range(q.qsize()): print(q.get_nowait())

"""

运行结果:

False True 消息列队已满,现有消息数量:3 消息列队已满,现有消息数量:3 消息1 消息2 消息3
"""

说明

初始化Queue()对象时(例如:q=Queue()),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就表明可接受的消息数量没有上限(直到内存的尽头);

  • Queue.qsize():返回当前队列包含的消息数量;

  • Queue.empty():若是队列为空,返回True,反之False ;

  • Queue.full():若是队列满了,返回True,反之False;

  • Queue.get([block[, timeout]]):获取队列中的一条消息,而后将其从列队中移除,block默认值为True;

    1)若是block使用默认值,且没有设置timeout(单位秒),消息列队若是为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,若是设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出"Queue.Empty"异常;

    2)若是block值为False,消息列队若是为空,则会马上抛出"Queue.Empty"异常;

  • Queue.get_nowait():至关Queue.get(False);

  • Queue.put(item,[block[, timeout]]):将item消息写入队列,block默认值为True;

    1)若是block使用默认值,且没有设置timeout(单位秒),消息列队若是已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,若是设置了timeout,则会等待timeout秒,若还没空间,则抛出"Queue.Full"异常;

    2)若是block值为False,消息列队若是没有空间可写入,则会马上抛出"Queue.Full"异常;

  • Queue.put_nowait(item):至关Queue.put(item, False);

 

进程池Pool

当须要建立的子进程数量很少时,能够直接利用multiprocessing中的Process动态成生多个进程,但若是是上百甚至上千个目标,手动的去建立进程的工做量巨大,此时就能够用到multiprocessing模块提供的Pool方法。

初始化Pool时,能够指定一个最大进程数,当有新的请求提交到Pool中时,若是池尚未满,那么就会建立一个新的进程用来执行该请求;但若是池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用以前的进程来执行新的任务,请看下面的实例:

# -*- coding:utf-8 -*-
from multiprocessing import Pool import os, time, random def worker(msg): t_start = time.time() print("%s开始执行,进程号为%d" % (msg,os.getpid())) # random.random()随机生成0~1之间的浮点数
    time.sleep(random.random()*2) t_stop = time.time() print(msg,"执行完毕,耗时%0.2f" % (t_stop-t_start)) po = Pool(3)  # 定义一个进程池,最大进程数3
for i in range(0,10): # Pool().apply_async(要调用的目标,(传递给目标的参数元祖,))
    # 每次循环将会用空闲出来的子进程去调用目标
 po.apply_async(worker,(i,)) print("----start----") po.close() # 关闭进程池,关闭后po再也不接收新的请求
po.join()  # 等待po中全部子进程执行完成,必须放在close语句以后
print("-----end-----")

""" 运行结果:
----start---- 0开始执行,进程号为21466 1开始执行,进程号为21468 2开始执行,进程号为21467 0 执行完毕,耗时1.01 3开始执行,进程号为21466 2 执行完毕,耗时1.24 4开始执行,进程号为21467 3 执行完毕,耗时0.56 5开始执行,进程号为21466 1 执行完毕,耗时1.68 6开始执行,进程号为21468 4 执行完毕,耗时0.67 7开始执行,进程号为21467 5 执行完毕,耗时0.83 8开始执行,进程号为21466 6 执行完毕,耗时0.75 9开始执行,进程号为21468 7 执行完毕,耗时1.03 8 执行完毕,耗时1.05 9 执行完毕,耗时1.69 -----end-----
"""

multiprocessing.Pool经常使用函数解析:

  • apply_async(func[, args[, kwds]]) :使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
  • close():关闭Pool,使其再也不接受新的任务;
  • terminate():无论任务是否完成,当即终止;
  • join():主进程阻塞,等待子进程的退出, 必须在close或terminate以后使用;

 

进程池中的Queue

若是要使用Pool建立进程,就须要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),不然会获得一条以下的错误信息:

RuntimeError: Queue objects should only be shared between processes through inheritance.

下面的实例演示了进程池中的进程如何通讯:

# -*- coding:utf-8 -*-

# 修改import中的Queue为Manager
from multiprocessing import Manager,Pool import os,time,random def reader(q): print("reader启动(%s),父进程为(%s)" % (os.getpid(), os.getppid())) for i in range(q.qsize()): print("reader从Queue获取到消息:%s" % q.get(True)) def writer(q): print("writer启动(%s),父进程为(%s)" % (os.getpid(), os.getppid())) for i in "itcast": q.put(i) if __name__=="__main__": print("(%s) start" % os.getpid()) q = Manager().Queue()  # 使用Manager中的Queue
    po = Pool() po.apply_async(writer, (q,)) time.sleep(1)  # 先让上面的任务向Queue存入数据,而后再让下面的任务开始从中取数据
 po.apply_async(reader, (q,)) po.close() po.join() print("(%s) End" % os.getpid()) """ 运行结果: writer启动(11097),父进程为(11095) reader启动(11098),父进程为(11095) reader从Queue获取到消息:i reader从Queue获取到消息:t reader从Queue获取到消息:c reader从Queue获取到消息:a reader从Queue获取到消息:s reader从Queue获取到消息:t """

 

协程:
协程,又称微线程,纤程。英文名Coroutine。

 

协程是啥:
协程是python个中另一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为须要的资源)。
通俗的理解:在一个线程中的某个函数,能够在任何地方保存当前函数的一些临时变量等信息,而后切换到另一个函数中执行,注意不是经过调用函数的方式作到的,而且切换的次数以及何时再切换到原来的函数都由开发者本身肯定

协程和线程差别:
在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操做系统为了程序运行的高效性每一个线程都有本身缓存Cache等等数据,操做系统还会帮你作这些数据的恢复操做。 因此线程的切换很是耗性能。可是协程的切换只是单纯的操做CPU的上下文,因此一秒钟切换个上百万次系统都抗的住。

 

gevent
greenlet已经实现了协程,可是这个还的人工切换,是否是以为太麻烦了,不要捉急,python还有一个比greenlet更强大的而且可以自动切换任务的模块gevent
其原理是当一个greenlet遇到IO(指的是input output 输入输出,好比网络、文件操做等)操做时,好比访问网络,就自动切换到其余的greenlet,等到IO操做完成,再在适当的时候切换回来继续执行。
因为IO操做很是耗时,常常使程序处于等待状态,有了gevent为咱们自动切换协程,就保证总有greenlet在运行,而不是等待IO


进程、线程、协程对比
请仔细理解以下的通俗描述
• 有一个老板想要开个工厂进行生产某件商品(例如剪子)
• 他须要花一些财力物力制做一条生产线,这个生产线上有不少的器件以及材料这些全部的 为了可以生产剪子而准备的资源称之为:进程
• 只有生产线是不可以进行生产的,因此老板的找个工人来进行生产,这个工人可以利用这些材料最终一步步的将剪子作出来,这个来作事情的工人称之为:线程
• 这个老板为了提升生产率,想到3种办法:
1. 在这条生产线上多招些工人,一块儿来作剪子,这样效率是成倍増长,即单进程 多线程方式
2. 老板发现这条生产线上的工人不是越多越好,由于一条生产线的资源以及材料毕竟有限,因此老板又花了些财力物力购置了另一条生产线,而后再招些工人这样效率又再一步提升了,即多进程 多线程方式
3. 老板发现,如今已经有了不少条生产线,而且每条生产线上已经有不少工人了(即程序是多进程的,每一个进程中又有多个线程),为了再次提升效率,老板想了个损招,规定:若是某个员工在上班时临时没事或者再等待某些条件(好比等待另外一个工人生产完谋道工序 以后他才能再次工做) ,那么这个员工就利用这个时间去作其它的事情,那么也就是说:若是一个线程等待某些条件,能够充分利用这个时间去作其它事情,其实这就是:协程方式


简单总结
 1. 进程是操做系统资源分配的单位
 2. 线程是CPU调度的单位
 3. 进程切换须要的资源最大,效率很低
 4. 线程切换须要的资源通常,效率通常(固然在不考虑GIL的状况下)
 5. 协程切换任务资源很小,效率高
 6. 多进程、多线程根据cpu核数不同多是并行的,可是协程是在一个线程中 因此是并发

相关文章
相关标签/搜索