-- 多任务-多线程-多进程-协程-进阶学习 --
文中所提到的案例参考:GITHUB中项目文件夹
https://github.com/FangbaiZhang/Python_advanced_learning/tree/master/02_Python_advanced_grammar_supplement/002_%E5%A4%9A%E7%BA%BF%E7%A8%8B_%E5%A4%9A%E8%BF%9B%E7%A8%8B_%E5%8D%8F%E7%A8%8B_%E8%BF%9B%E9%98%B6%E6%B1%87%E6%80%BB
1 多任务
1.1 多任务概念
- 多任务举例
什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。
打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,
至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。 - 电脑多核单核
多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。
由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,
任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。
表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,
我们感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,
所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。 - 并发与并行概念
并发:指的是任务数大余cpu核数,通过操作系统的各种任务调度算法,
实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,每个cup核同时执行不同的任务,即所有的任务真的是一起执行的(物理意义上的多任务)
- 当多进程的任务数小于CPU的核数,多个任务同时执行,可以实现物理意义上的并行
- 但实际中,多进程任务数远远大于CPU核数,就只能通过调度轮流执行,实现多并发
- 关于并发、并行区别与联系
如果你的代码是IO密集型的,线程和多进程可以帮到你。多进程比线程更易用,但是消耗更多的内存。
如果你的代码是CPU密集型的,多进程就明显是更好的选择——特别是所使用的机器是多核或多CPU的。
对于网络应用,在你需要扩展到多台机器上执行任务,RQ是更好的选择。
并发是指,程序在运行的过程中存在多于一个的执行上下文。这些执行上下文一般对应着不同的调用栈。
在单处理器上,并发程序虽然有多个上下文运行环境,但某一个时刻只有一个任务在运行。
但在多处理器上,因为有了多个执行单元,就可以同时有数个任务在跑。
这种物理上同一时刻有多个任务同时运行的方式就是并行。
和并发相比,并行更加强调多个任务同时在运行。
而且并行还有一个层次问题,比如是指令间的并行还是任务间的并行。 - 多进程和多线程区别:
- 多进程:多个代码片段在独立同时进行,可以同时利用多个内核,真正的多并发,同一时刻占用内存为多个进程之和
- 多线程:多个代码片段同时执行,由于存在GIL,本质上,同时只有一个线程再执行,只有一个进程占用内存
这个不执行,下一个才执行,内部先获取锁,释放锁,并不能发挥多核性能 - 因此,多线程是伪多并发,同一时刻占用内存还是一个进程的
- Python使用多进程才能利用CPU的多核资源
- 当多进程的任务数小于CPU的核数,多个任务同时执行,可以实现物理意义上的并行
- 但实际中,多进程任务数远远大于CPU核数,就只能通过调度轮流执行,实现多并发
- 全局解释器(GIL)
- Python代码的执行是由python虚拟机进行控制
- Python语言本身并没有GIL问题,是python虚拟机(Python解释器)的问题
- 在主循环中只能有一个线程在执行,每个线程执行的时候都需要先获取GIL,
- 以保证同一时刻只有一个线程在执行代码
- 线程释放GIL锁的情况:
- 在IO操作等可能会引起阻塞的system call之前,可以暂时释放GIL,
- 但在执行完毕后,必须重新获取GIL
- 解决多线程的GIL问题:
- 方法1:更换python解释器(默认解释器是C语言编写的),换成java写的解释器jypython
- 方法2:用其它语言替代多线程代码,python胶水语言,可以直接调用c语言,java语言等写的代码
- 多线程和多进程的使用
- 计算密集型(多进程):代码没有等待时间,使用多进程,发挥多核的性能
- IO密集型(多线程,协程):(input,output),比如网络收发,文件读写,具有等待时间,优先使用协程,再考虑多线程
- 比如:请求网络,发出请求,假设需要等待时间1分钟,然后返回内容,
- 这个等待时间就可以先解锁当前线程,因为还在等待返回内容,开始下个线程
- 等待的1分钟内可以发出多个请求,返回内容回来时候,再次请求锁,不断上锁解锁
- 网页爬取:多线程比单线程爬取性能有提升,因为遇到IOS阻塞会自动释放GIL锁
2. 线程
- 多线程是伪多并发
多个代码片段同时执行,由于存在GIL,本质上,同时只有一个线程再执行,
这个不执行,下一个才执行,内部先获取锁,释放锁
因此,多线程是伪多并发,占用内存还是一个进程的 - 线程
- 一个进程的独立运行片段,一个进程可以有多个线程
- 轻量化的进程
- 一个进程的多个线程共享数据和上下文运行环境
- 多线程共享全局变量
- 共享互斥问题
- 线程常用属性
- threading.currentThread: 返回当前的线程变量
- threading.enumerate(): 返回一个包含正在运行所有线程的list。
- 正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量,
- 与len(threading.enumerate())有相同的结果。
- 除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
run(): 用以表示线程活动的方法。
start():启动线程活动。内部调用系统的多个函数,启动多线程。
join([time]): 等待至线程中止。这阻塞调用线程直至
线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
isAlive(): 返回线程是否活动的。
getName(): 返回线程名。
setName(): 设置线程名。
- 单线程多线程参考:001_多线程文件夹中的001-005案例
- 线程执行顺序
多线程start()之后执行的循序不确定,并不一定按启动顺序执行
要想按顺序执行,可以加入延时功能
参考案例-001_多线程文件夹中的006/007/008/009 - 线程封装-类继承
通过使用threading模块能完成多任务的程序开发,
为了让每个线程的封装性更完美,所以使用threading模块时,
往往会定义一个新的子类class,只要继承threading.Thread就可以了,
然后重写run方法
python的threading.Thread类有一个run方法,
用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。
而创建自己的线程实例后,通过Thread类的start方法,
可以启动该线程,交给python虚拟机进行调度,
当该线程获得执行的机会时,就会调用run方法执行线程。
每个线程默认有一个名字,尽管案例中没有指定线程对象的name,
但是python会自动为线程指定一个名字。
当线程的run()方法结束时该线程完成。
无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。
参考案例-001_多线程文件夹中的010/011/012
005文件夹中的28_9 - 线程共享全局变量问题
在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)
如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确
参考案例:005文件夹中的28_10/28_10_1 - 互斥锁(上锁解锁,解决变量冲突的问题)
上锁解锁过程
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,
称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
参考案例:005文件夹中的28_11/28_11_1
总结:
锁的好处:
确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
阻止了多线程并发执行(一个线程的锁执行时候,另外一个线程的锁必须等待,此时CPU的多核就存在浪费,没有利用起来实现多并发),
包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
参考案例:005文件夹中的28_11/28_11_1 - 死锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但一旦发生就会造成应用的停止响应。
避免死锁:
程序设计时要尽量避免(银行家算法)
添加超时时间等
银行家算法:
银行家算法是从当前状态出发,逐个按安全序列检查各客户谁能完成其工作,
然后假定其完成工作且归还全部贷款,再进而检查下一个能完成工作的客户,
如果所有客户都能完成工作,则找到一个安全序列,银行家才是安全的。
因此,程序有多个锁时,提前计算好执行顺序,什么时候解锁,什么时候上锁,避免出现冲突 - 多任务UDP聊天器
参考案例014
3. 进程
3.1 进程概念
- 程序:
例如xxx.py这是程序,是一个静态的 - 进程:
一个程序运行起来后,代码+用到的资源称之为进程,它是操作系统分配资源的基本单元。 - 多进程:
不仅可以通过线程完成多任务,进程也是可以的。
多进程同时多个代码同时执行,可以调度CPU的多核心,是真正的多并发
一个线程和一个进程里面不一定只有一个.py,开始import导入了包,
调用了多个py多个函数,其实一个线程或进程都会执行多个.py代码
3.2 进程和线程的区别
-进程与线程举例
进程,能够完成多任务,比如 在一台电脑上能够同时运行多个QQ
线程,能够完成多任务,比如 一个QQ中的多个聊天窗口
- 线程:
- 一个进程的独立运行片段,一个进程可以有多个线程
- 轻量化的进程
- 一个进程的多个线程共享数据和上下文运行环境
- 多线程共享全局变量
- 共享互斥问题
- 进程:程序运行的一个状态
- 包含地址空间,内存,数据栈等资源
- 每个进程有自己独立的运行环境
- 多进程共享数据是一个问题
- 任务管理器中查看,可以运行运行多个网络调试助手或QQ等程序,就是多个进程
- 进程占用的资源更多:
001多进程案例:
代码+资源 主进程 (main)
代码+资源 子进程1 (p1)
代码+资源 子进程2 (p2)
相对于多线程,占用的资源更多了 - 区别总结
- 定义的不同
进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,
它是比进程更小的能独立运行的基本单位.
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源
(如程序计数器,一组寄存器和栈),
但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源. - 联系
一个程序至少有一个进程,一个进程至少有一个线程.
进程是电脑资源分配的单位,线程是操作系统调度的单位
线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
线程不能够独立执行,必须依存在进程中,先有进程再有线程
可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人
- 优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
3.3 进程创建、通信
- 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(进程号) - 查看002文件夹中的案例001/002/003
3.4 进程通信multiprocessing.Queue
- 初始化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);
设置True,超时5秒后抛出异常,设为False立即抛出异常
q.get(True, 5))
print(q.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);
设置为True,超时2秒后抛出异常
q.put(“消息4”, True, 2)
- 参考案例004/005/006
3.5 进程池multiprocessing.Pool
- 当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,
但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,
此时就可以用到multiprocessing模块提供的Pool方法。 - 初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,
如果池还没有满,那么就会创建一个新的进程用来执行该请求;
但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,
直到池中有进程结束,才会用之前的进程来执行新的任务
进程池的特点就是会重复利用进程池中的进程,可查看案例中的pid很多是相同的
- p = Pool() 未传入参数,则默认是CPU核数
- 新开始的进程使用的是原来的进程
- 结束和新开始的进程的进程代码pid是一样的(重复利用进程池中的进程)
- multiprocessing.Pool常用函数解析:
apply_async(func[, args[, kwds]]) :
使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),
args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
close():关闭Pool,使其不再接受新的任务;
terminate():不管任务是否完成,立即终止;
join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;
- 注意:Pool进程池必须加上join(),不然主进程不会等待子进程,主进程结束后子进程直接挂掉
Process的进程默认会等待子进程结束后主进程才结束
参考002文件中的007案例
参考007文件夹中1.4.2案例
- 进程池通信Queue
如果要使用Pool创建进程通信,就需要使用multiprocessing.Manager()中的Queue(),
而不是multiprocessing.Queue(),否则会得到一条如下的错误信息:
RuntimeError: Queue objects should only be shared between processes through inheritance.
参考002文件中的008案例
3.6 多任务多进程————文件夹COPY
- 流程(大量文件复制,进程池实现多任务)
- 获取用户要copy的文件夹的名字
- 创建一个新的文件夹
- 获取原文件夹中所有待copy文件的名字 listdir()
- 创建进程池
- 创建消息队列
- 向进程池中添加copy文件的任务
创建一个复制文件的函数 - 显示出复制进度条
- 参考002文件夹中的009/010/011/012案例
4 协程
4.1 迭代器与可迭代对象(Iterator Iterable)
- 迭代器概念
- 迭代是访问集合元素的一种方式。
- 迭代器是一个可以记住遍历的位置的对象。
- 迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。
- 迭代器只能往前不会后退。
- 可迭代对象
可以通过for…in…这类语句迭代读取一条数据
供我们使用的对象称之为可迭代对象(Iterable) - 可迭代对象与迭代器的联系
对可迭代对象进行迭代使用的过程,发现每迭代一次(即在for…in…中每循环一次)
都会返回对象中的下一条数据,一直向后读取数据直到迭代了所有数据后结束。
那么,在这个过程中就应该有一个“人”去记录每次访问到了第几条数据,
以便每次迭代都可以返回下一条数据。
我们把这个能帮助我们进行数据迭代的“人”称为迭代器(Iterator) - 可迭代对象的本质
本质就是可以向我们提供一个这样的中间“人”即迭代器帮助我们对其进行迭代遍历使用。
可迭代对象通过__iter__方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,
实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据.
那么也就是说,一个具备了__iter__方法的对象,就是一个可迭代对象 - 迭代器的本质
当我们对迭代器使用next()函数的时候,迭代器会向我们返回它所记录位置的下一个位置的数据。
实际上,在使用next()函数的时候,调用的就是迭代器对象的__next__方法
(Python3中是对象的__next__方法,Python2中是对象的next()方法)。
所以,我们要想构造一个迭代器,就要实现它的__next__方法。
但这还不够,python要求迭代器本身也是可迭代的,所以我们还要为迭代器实现__iter__方法,
而__iter__方法要返回一个迭代器,迭代器自身正是一个迭代器,所以迭代器的__iter__方法返回自身即可。
一个实现了__iter__方法和__next__方法的对象,就是迭代器 - 参考案例002/003/004/005
- 同时参考001_11_迭代器与生成器(yield_iterator)文件夹中向相关案例
- for循环本质
for item in Iterable 循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,
然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,
当遇到StopIteration的异常后循环结束。
for循环执行步骤:
- 判断mylist是否是一个可迭代对象。(mylist里面实现了__iter__方法,是可迭代对象)
- 第1步成立的情况下,调用__iter__方法,得到__iter__方法的返回值。
- __iter__方法的返回值是一个迭代器对象。(MyIterator()就是一个迭代器对象,包含了__iter__和__next__方法)
- for循环会自动不断的调用迭代器对象中的__next__方法,得到该方法的返回值
- 因此最后,i的打印结果就是111,并且会不断打印下去,因为__next__方法一直有返回值111
- 执行步骤参考案例004
4.2 迭代器应用
- 迭代器就像一个懒加载的工厂,等到有人需要的时候才给它生成值返回,
没调用的时候就处于休眠状态等待下一次调用。直到无元素可调用,
返回StopIteration异常。 - 迭代器最核心的功能就是可以通过next()函数的调用来返回下一个数据值。
如果每次返回的数据值不是在一个已有的数据集合中读取的,
而是通过程序按照一定的规律计算生成的,那么也就意味着可以不用再依赖一个已有的数据集合,
也就是说不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,
这样可以节省大量的存储(内存)空间
—迭代器保存的不是大量数据,而是得到数据的方法—
- 参考006案例:
使用的是while循环一次性缓存了大量数据nums,保存在一个列表中,占用了大量内存
然后使用for循环从列表中取出数据,但是取出就会调用迭代器,for循环取出时实现了迭代器的功能 - 007案例:
是用迭代器实现生成数据的方法,缓存中并不会保存每一个数据,保存的是生成数据的方法
保存的某个数生成前后的一个状态,底层的实现就是中断的原理,保存栈帧,加载栈帧
然后通过for循环从迭代器中不断调用next方法取出数据
4.3 生成器(generator)
- 生成器与迭代器关系
利用迭代器,我们可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成。
但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据。
为了达到记录当前状态,并配合next()函数进行迭代使用,我们可以采用更简便的语法,
即生成器(generator)。生成器是一类特殊的迭代器。 - yield和return的关系和区别了,
带yield的函数是一个生成器,
而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,
这一次的next开始的地方是接着上一次的next停止的地方执行的,
所以调用next的时候,生成器并不会从foo函数的开始执行,只是接着上一步停止的地方开始,
然后遇到yield后,return出要生成的数,此步就结束。
generator生成器是用来产生一系列值的,yield则像是generator生成器函数的返回结果,(yield也可以看似return)
yield唯一所做的另一件事就是保存一个generator函数的状态
(yield和return的区别,return执行后会继续执行后面的代码,但是yield会停止之后的代码继续执行,
注意,只是停止生成器函数内部的代码,生成器函数外部代码不受影响)
generator就是一个特殊类型的迭代器(iterator),和迭代器相似,我们可以通过使用next()来从generator中获取下一个值
参考001_11_迭代器与生成器(yield_iterator)文件夹中003案例
- yield生成器的优点
yield生成器和迭代器一样,保存的不是大量数据,而是得到数据的方法
yield的好处:
- 不会将所有数据取出来存入内存中;而是返回了一个对象;可以通过对象获取数据;用多少取多少,可以节省内寸空间。
- 除了能返回一个值,还不会终止循环的运行;
- 每次执行到yield,因为底层的实现就是中断的原理,保存栈帧,加载栈帧。
- 每次执行结束内存释放,执行的时候占用一点内存,消耗的内存资源就很少
参考001_11_迭代器与生成器(yield_iterator)文件夹中005/006/007案例
- yield应用
爬虫经常使用到yield request,yield item
就是利用的协程实现的多任务多并发
可以参考Spider_development_study_note中ch12中cnblogSpider和shtspider
爬虫主程序中,yield直接写在for循环内部
parse函数内部有for循环,有request,
调用了parse_body(代码中有yield item),for循环最后是yield request
爬虫代码运行时候,for循环自动调用next方法,yield就会不断执行,直到爬取结束
使用yield也会大大减少内存的消耗
- yield生成器函数总结
使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)
yield关键字有两点作用:
- 保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
- 将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用
yield函数访问的三种方式:
- next(函数名)
- 函数名.send(参数) 参数可以为None
- 函数名.next() 不推荐
- for循环访问,自动调用next函数,自己处理异常
可以使用next()函数或send函数让生成器从断点处继续执行,即唤醒生成器(函数)
Python3中的生成器可以使用return返回最终运行的返回值,
而Python2中的生成器不允许使用return返回一个返回值
(即可以使用return从生成器中退出,但return后不能有任何表达式)。
参考009/010/011案例 - 迭代器和生成器(特殊的迭代器)最终目的:
- 迭代器减少节约内存空间(迭代器保存的不是大量数据,而是得到数据的方法),能实现循环生成大量数据
- 生成器可以让一个函数暂停执行,根据自己需要继续执行函数
4.4 协程
- 协程,又称微线程,纤程。英文名Coroutine。
- 协程是什么?
协程是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)。
为啥说它是一个执行单元,因为它自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程切换到另一个协程。
只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。
通俗的理解:
在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,
然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,
并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定 - 协程和线程差异
在实现多任务时, 线程切换从系统层面远不止保存和恢复CPU上下文这么简单。
操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,
操作系统还会帮你做这些数据的恢复操作。所以线程的切换非常耗性能。
协程调用一个任务就类似于调用一个函数一样,消耗的资源最小
协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
5. 总结
- 多进程和多线程区别:
- 多进程:多个代码片段在独立同时进行,可以同时利用多个内核,真正的多并发,同一时刻占用内存为多个进程之和
- 多线程:多个代码片段同时执行,由于存在GIL,本质上,同时只有一个线程再执行,只有一个进程占用内存
这个不执行,下一个才执行,内部先获取锁,释放锁,并不能发挥多核性能 - 因此,多线程是伪多并发,同一时刻占用内存还是一个进程的
- Python使用多进程才能利用CPU的多核资源
- 当多进程的任务数小于CPU的核数,多个任务同时执行,可以实现物理意义上的并行
- 但实际中,多进程任务数远远大于CPU核数,就只能通过调度轮流执行,实现多并发
- 多线程和多进程的使用
- 计算密集型(多进程):代码没有等待时间,使用多进程,发挥多核的性能
- IO密集型(多线程,协程):(input,output),比如网络收发,文件读写,具有等待时间,优先使用协程,然后考虑线程
- 比如:请求网络,发出请求,假设需要等待时间1分钟,然后返回内容,
- 这个等待时间就可以先解锁当前线程,因为还在等待返回内容,开始下个线程
- 等待的1分钟内可以发出多个请求,返回内容回来时候,再次请求锁,不断上锁解锁
- 网页爬取:多线程比单线程爬取性能有提升,因为遇到IOS阻塞会自动释放GIL锁
- 协程,线程,进程的关系
- 一个py文件运行起来就是一个进程(也可以有一个主进程多个子进程),
- 进程里面有至少有一个线程(可看做主线程),
- 主线程可以有多个子线程,线程里面有协程
- 协程依赖线程,线程依赖进程
- 一个线程中可以有多个协程,一个进程中可以有多个线程,一个主进程中可以有多个子进程
- 切换时候消耗的资源:进程>线程>协程,并且相差巨大
- 协程,线程,进程简单总结
进程是资源分配的单位
线程是操作系统调度的单位
进程切换需要的资源很最大,效率很低
线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
协程切换任务资源很小,效率高
多进程根据cpu核数不一样可能是并行的,多线程由于存在GIL是伪多并发
但是协程是在一个线程中所以只能是并发