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

多线程:

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)状态。

 

多进程:

程序:比如电脑安装了很多程序,又比如我们编写一个xxx.py程序,它们静静的保存在硬盘中,所以程序是一个静态的概念
进程:一个程序运行起来后,代码+用到的资源 称之为进程,它是操作系统分配资源的基本单位。

 1.进程的状态
工作中,任务数往往大于cpu的核数,即一定有一些任务正在执行,而另外一些任务在等待cpu进行执行,因此导致了有了不同的状态

 • 就绪态:运行的条件都已经满足,正在等在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核数不一样可能是并行的,但是协程是在一个线程中 所以是并发