多任务
- 并发:同一时间段 执行多个任务 —> 如:单核 基于时间片的CPU轮转
- 并行:同一时间点 执行多个任务 —> 如:多核
- 实现方式有:进程,线程,协程(代码层)
原谅我的低级画功
进程vs线程
- 进程是资源分配的独立单位,线程是操作系统调度的基本执行单位
- 一个程序中默认有一个主进程,一个进程中默认有一个主线程
- 进程间是不共享数据,线程间是共享数据
- 多进程和多线程的执行顺序是无序的
- 程序会等待所有进程结束再关闭退出,进程结束则关闭释放资源
- 进程结束,此进程里的多线程会强制关闭释放资源,可以在进程中加上time.sleep来暂缓进程的结束
多线程:(cpython中存在GIL问题)
- 创建开启子线程
- 创建Thread类对象
python
import threading # 实际上threading 是对 Thread 二次封装 使之更易用
thd = threading.Thread(target=funcName, args=(位置参数,), kwargs={key:val,...})
- 开启线程
python
thd.start()
- 创建Thread类对象
等待子线程结束 执行下面
join():阻塞等待某个子线程结束 —-> 多个线程则需要 多个 thd.join()
import time import threading def sayHello(no, age=10): for i in range(2): print("hell0", no) time.sleep(1) if __name__ == '__main__': for i in range(3): # 位置参数用args=元组 关键字参数用kwargs={key:val} thd = threading.Thread(target=sayHello, args=(i,), kwargs={"age": 100}) thd.start() # 阻塞等待子线程 thd.join() print(6666)
threading.enumerate():查看当前线程数量 当线程数量为1时,则子线程都结束
while True: if len(threading.enumerate()) == 1: break else: time.sleep(1) ....
创建子线程的另一方式(继承threading.Thread方便代码封装)
import threading import time class MyThread(threading.Thread): def run(self): """需要子线程执行的代码""" for i in range(100): print("这是子线程") time.sleep(1) # 创建子类对象 mt = MyThread() # 调用子类对象.start() mt.start() # 即一个mt子线程 for i in range(10): print("这是主线程") time.sleep(1)
多线程间是共享数据(因为在同一个进程中),同时修改同一数据时可能 导致 数据竞争
"""两个线程对一个全局变量g_number 数学加1""" import threading g_number = 0 def hello(): for i in range(1000000): # 加的次数越大越容易出现资源竞争问题 global g_number g_number += 1 def world(): for i in range(1000000): global g_number g_number += 1 if __name__ == '__main__': hello_thd = threading.Thread(target=hello) world_thd = threading.Thread(target=world) hello_thd.start() world_thd.start() # 阻塞等待 hello_thd.join() world_thd.join() print(g_number) # 结果随机 可能小于2000000 # 小总结: 多线程的执行顺序是随机的 # 假设某个时间点g_number = 1000. # 线程1 对g_number所指向的内存地址中的数字(1000)加1; # 在这个时间点上,线程2也访问到g_number所指向的内存地址中的数字(1000),然在加1; # 这个情况下,g_number实际上只被加1
- 解决办法:互斥锁— 包含锁的多线程程序 其实就变成了并发模式,好处:保证正确执行;坏处:效率大大降低,处理的不好容易产生死锁
lock = threading.Lock() # 申请锁 lock.acquire() # 加锁 并阻塞,返回TorF 判断是否成功 成功往下,反之阻塞 待加锁代码块 lock.release() # 解锁 并解除阻塞 # 这个原理其实是 冻结 俩个代码块 ,释放一个才会执行第二个代码块 相当于两个车道,在特定的范围,变单车道
"""解决多线程共享数据引起竞争的问题-互斥锁""" import threading g_number = 0 def hello(lock): for i in range(10): global g_number # 申请加锁 lock.acquire() g_number += 1 # 释放互斥锁 # time.sleep(1) print('hello') lock.release() def world(lock): for i in range(10): global g_number lock.acquire() g_number += 1 print("world") lock.release() if __name__ == '__main__': # 申请一个锁 lock = threading.Lock() # 将锁传入线程中 hello_thd = threading.Thread(target=hello,args=(lock,)) world_thd = threading.Thread(target=world,args=(lock,)) hello_thd.start() world_thd.start() hello_thd.join() world_thd.join() print(g_number)
- 解决办法:互斥锁— 包含锁的多线程程序 其实就变成了并发模式,好处:保证正确执行;坏处:效率大大降低,处理的不好容易产生死锁
- 避免死锁办法
- 死锁的产生
多线程中加锁的程序中, 两个锁住的代码相互引用,导致资源不能释放(死锁) - 银行家算法
- 资源的合理利用的方法
- 添加超时时间(不太好)
- lock.acquire(True,10)
- 第一个参数默认True:是否阻塞等待
- 第二个参数默认-1:表示一直阻塞 改为10:10秒后解除锁不继续阻塞
- lock.acquire(True,10)
- 死锁的产生
扩展
GIL:全局解释器锁(相当于一个大的互斥锁)
c语言写的python解释器(官方)中存在,一个进程中有一个GIL来控制多线程之间资源资源,所以cpython中多线程为并发.无论多少核cpu,同一时间点,只有一个线程在运行,不能完全利用CPU(利用率永远达不到100%,多进程可以)
eg:import threading, multiprocessing def loop(): x = 0 while True: x = x ^ 1 # multiprocessing.cpu_count()就是电脑的cup核数,我分配了4核(如下图) for i in range(multiprocessing.cpu_count()): t = threading.Thread(target=loop) t.start()
GIL产生历史原因
龟叔编写python的年代电脑只有单核,为了实现在单核CPU上的多线程,便使用的了GIL来实现多线程,在那时这个是很了不起的,只是后来电脑拥有多核CPU是不适用了.
但是以前的代码量太多,现在在修改就很难,一直保留下来.- GIL解决方法
- 使用其他语言写的python解释器(
不推荐
,还是用官方CPython好)
eg:Jython(java);IronPython(.net);pypy(Python) - 不使用多线程,使用多进程-进程里加协程实现多任务来充分利用多核CPU (
推荐
) - 引用c语言实现的多线程模块
- 使用其他语言写的python解释器(
- 即使存在GIL 在有IO等待操作的程序中,还是多线程快,当然没有资源等待的还是单线程快(科学计算,累加等等)
多进程
进程的三个典型状态切换 —1.就绪态(ready);2.运行态(running);3.阻塞态(block)
实现多进程实际上就是子进程复制父进程的资源,所以资源不共享
声明开启子进程:
import multiprocessing # 声明子进程变量 pro = multiprocessing.Process(target=funcName, args=(xx,)) pro.start() # 给操作系统发送一个创建执行子进程的信号,这个过程是需要一定的时间
pro.is_alive():判断进程是否存活
- pro.join([timeout]):一直阻塞子进程pro结束,或阻塞多少秒就不管了
- pro.terminate():不管任务是否完成,立即终止子进程
注:这个立即结束是向操作系统发出终结命令,so需要等待一定时间(大概0.01s).
一般是terminate()然后join这两个一起用 进程间的通信–队列(queue) 特点:先进先出
- 创建一个队列 原理就是多个子进程共用一个队列
multiprocessing.Queue([maxsize])
- 队列的常用方法
- Queue.qsize() 返回当前队列的消息数量
- Queue.empty() 如果队列为空,返回T
- Queue.full() 如果队列满了,返回T;当队列满了时,再添加元素,会阻塞等待直至添加成功
- Queue.get([block[,timeout]]) 获取队列中第一条消息,然后从队列移除,block ,默认True阻塞效果,有时间设置的话,时间到还阻塞就抛异常
- Queue.get_nowait() 相当于Queue.get(False) 拿不到直接抛异常,没有阻塞效果
- Queue.put(item[,timeout]) 往队列队尾放数据,队列满时,会阻塞,直到有位置放数据,timeout时间到且size==maxsize则抛出异常
- Queue.put_nowait() 相当于Queue.put(item,False) 往队列中放数据放不了则直接抛异常
- 例子:
import multiprocessing import time def func(queue): if not queue.empty(): for i in range(queue.qsize()): print(queue.get_nowait()) # get_nowait() ===get(True,0) 0是时间默认-1 T阻塞状态 time.sleep(1) def main(): # 创建一个进程间通信队列 queue = multiprocessing.Queue(3) # 声明一个进程 pro = multiprocessing.Process(target=func,args=(queue,)) # 创建一个进程并开启 pro.start() for i in range(3): queue.put_nowait('消息%s'%i) if not queue.full(): queue.put_nowait("消息4") # put_nowait()===put('xxdata',True,0) 0是时间默认-1 else: print("消息队列满了") pro.join() pro.terminate() # 杀死进程 a= pro.is_alive() # 判断是否还活着 有一定的延时行 print(a) if __name__=="__main__": main()
- 创建一个队列 原理就是多个子进程共用一个队列
进程的pid
pro.pid
这个是属性值;或者os.getpid()
扩展
- 进程是操作系统资源分配的基本单位
- 进程间是独立的数据空间,相当于子进程复制了一份代码出来,so不共享全局变量,但是一个进程中的多线程是共享全局变量的.
ps -aux
: 查看系统中所有进程信息
ps -ef
: 可以查看父级的ip(ppid)
top
:动态显示进程信息
htop
:动态显示进程信息,并显示系统资源使用情况- 孤儿进程:父进程挂了,子进程还在运行
父进程回收子进程资源通过其init来回收,父进程挂了就由系统的init来
僵死进程:子进程退出,父进程没有回收子进程的资源.坏处:占用系统资源
但是python是高级语言,不用处理,可以自动回收,下一次再创建子进程时会复用之前的僵死进程资源,目的是为了减少资源的浪费,并且加速进程的创建
进程池
进程池概论
提前创建一定数量的进程,任务数多于进程池最大进程数量时,就等待某个进程结束,来运行下个任务;重复使用这些进程
特点:
节省重复创建进程的时间以及销毁的系统开销
自动会进程池中的进程进行管理
提高了对用户需求的响应效果工作进程 — 就是进程池中的进程
管理进程 — 就是主进程,维护进程池,也叫控制进程- 步骤
import multiprocessing
def funct_xxx(xx):
print(xx)
if __name__ == '__main__':
# 1,创建一个进程池
pool = multiprocessing.Pool(3) # 3代表最大进程数同时运行 ****但是一开始就已创建3个工作进程 加上主进程就是4个 不写就是系统最大的进程数 和CPU核数有关
# 2,创建进程池中的通信
queue = multiprocessing.Manager().Queue(4) # 用法和上面一致
# 3,添加任务
pool.apply(func=funct_xxx,args=(1,)) # 添加任务并且阻塞等待任务执行完成 能保证顺序
pool.apply_async(func=funct_xxx,args=(2,)) # 异步添加任务 不阻塞等待任务执行完成 ***用的多
# 4,关闭进程池
pool.close()
pool.join() ## 必须放在close或者terminate之后使用;
print("关闭进程池")
协程
协程是啥
- 协程,又称微线程,纤程(并发)
- 协程是提高单核最大效率,必须要有阻塞才会有效果提升
- 比线程还要少占用
原理切换-用户层面实现的切换机制
通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定
协程和线程差异
在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的 切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
简单实现协程
- yield
- 挂起当前执行的代码
- 恢复代码继续执行
import time
def a():
i = 0
while True:
print("a中", i)
i += 1
yield # 等待切换到另一个yield
time.sleep(1)
def b():
i = 0
while True:
print("b中", i)
i += 1
yield
time.sleep(1)
if __name__ == '__main__':
a1 = a()
b1 = b()
while True:
next(a1) # 唤起
next(b1) # 唤起
# a中 0
# b中 0
# a中 1
# b中 1
greenlet
为了更好使用协程来完成多任务,python中的greenlet模块对其封装(yield)
安装方式sudo pip3 install greenlet
from greenlet import greenlet import time def test1(): while True: print("---A--") gr2.switch() time.sleep(0.5) def test2(): while True: print("---B--") gr1.switch() time.sleep(0.5) gr1 = greenlet(test1) gr2 = greenlet(test2) # 切换到gr1中运行 gr1.switch()
gevent
gevent
是对greenlet
的二次封装,能够自动切换任务模块其原理是当一个greenlet遇到IO操作(指的是input output 输入输出,比如网络、文件操作等需要耗时等待的操作)时,就自动切换到其他的greenlet,等到IO操作文采,再在合适的时候切换回来继续执行
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
- 安装方式
pip3 install gevent
gevent
import gevent def f(n): for i in range(n): print(gevent.getcurrent(),i) gevent.sleep(1) # 阻塞一秒 g1 = gevent.spawn(f,5) g2 = gevent.spawn(f,5) g3 = gevent.spawn(f,5) g1.join() # 等待 不让 主进程关闭 g2.join() g3.join()
gevent 取消python耗时
from gevent import monkey import gevent import random import time # 有耗时操作时需要 monkey.patch_all() # 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块 def coroutine_work(coroutine_name): for i in range(10): print(coroutine_name, i) time.sleep(random.random()) gevent.joinall([ gevent.spawn(coroutine_work, "work1"), gevent.spawn(coroutine_work, "work2") ])
- 安装方式
简单总结
- 进程是资源分配的单位
- 线程是操作系统调度的单位
- 进程切换需要的资源很最大,效率很低
- 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
- 协程切换任务资源很小,效率高
- 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发
- 一般情况下,使用多进程加协程实现多任务