多任务
就是同一时间 内执行 多个任务
分为
- 并发 多个任务在一段时间内交替去执行,但是任务的切换速度快
- 并行 多个任务在一段时间内同时执行(任务数少于等于 cpu 核数)
- 串⾏:⼀个任务完完整整的执⾏完了再执⾏下⼀个
进程
进程:⼀个程序运⾏起来后,代码+⽤到的资源称之为进程,它是操作系统分配资源的基本单
元。
Process 进程类的说明
Process([group [, target [, name [, args [, kwargs]]]]])
group:指定进程组,目前只能使用 None
target:执行的目标任务名
name:进程名字
执行的任务带参数时候使用
args:以元组方式给执行任务传参(顺序一致)
kwargs:以字典方式给执行任务传参(键对应就行)
Process创建的实例对象的常用方法:
start():启动子进程实例(创建子进程)
join():等待子进程执行结束
terminate():不管任务是否完成,立即终止子进程
Process创建的实例对象的常用属性:
name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
#!/usr/bin/python
# -*- coding:UTF-8 -*-
import multiprocessing
import time
# 带有参数的任务
def task(count):
for i in range(count):
print("任务执行中..")
time.sleep(0.2)
else:
print("任务执行完成")
if __name__ == '__main__':
# 创建子进程
# args: 以元组的方式给任务传入参数,注意顺序
# sub_process = multiprocessing.Process(target=task, args=(5,))
# kwargs: 表示以字典方式传入参数,顺序无关,只要键对应
sub_process = multiprocessing.Process(target=task, kwargs={"count": 3})
sub_process.start()
进程的注意点介绍
- 进程之间不共享全局变量。(创建子进程会对主进程资源进行拷贝,也就是说子进程是主进程的一个副本,之所以进程之间不共享全局变量,是因为操作的不是同一个进程里面的全局变量,只不过不同进程里面的全局变量名字相同而已。)
- 主进程会等待所有的子进程执行结束再结束。
- 进程之间执行是无序的,它是由操作系统调度决定的,操作系统调度哪个进程,哪个进程就先执行,没有调度的进程不能执行。
保证主进程正常退出不等待子线程结束两种方法:
1. sub_process.daemon = True # 守护主进程就是主进程退出子进程销毁不再执行
2. sub.process.terminate() # 子进程销毁
线程
线程: CPU调度的基本单位。
多线程是并发的, CPU 来调度执行,也是依靠GIL 锁。
线程类 Thread 参数说明
Thread([group [, target [, name [, args [, kwargs]]]]])
- group:线程组,目前只能使用None target: 执行的目标任务名
- args: 以元组的方式给执行任务传参
- kwargs: 以字典方式给执行任务传参
- name: 线程名,一般不用设置
threading.enumerate() # 枚举所有活动的线程
threading.active_count() # 活动线程数
threading.current_thread() # 当前的线程
线程的注意点介绍
- 线程之间执行是无序的(线程之间执行是无序的,它是由cpu调度决定的 ,cpu调度哪个线程,哪个线程就先执行,没有调度的线程不能执行。)
- 主线程会等待所有的子线程执行结束再结束
- 线程之间共享全局变量,所以操作数据可能出现错误问题
线程同时操作解决方法:线程同步: 保证同一时刻只能有一个线程去操作全局变量 同步: 就是协同步调,按预定的先后次序进行运行。
设置守护主线程有两种方式:
threading.Thread(target=func, daemon=True) # 创建线程的时候就设置守护主线程
线程对象.setDaemon(True) # 线程开始前设置好守护主线程
线程同步
if __name__ == '__main__':
# 创建两个线程
first_thread = threading.Thread(target=sum_num1)
second_thread = threading.Thread(target=sum_num2)
# 启动线程
first_thread.start()
# 主线程等待第一个线程执行完成以后代码再继续执行,让其执行第二个线程
# 线程同步: 一个任务执行完成以后另外一个任务才能执行,同一个时刻只有一个任务在执行
first_thread.join()
# 启动线程
second_thread.start()
互斥锁
对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
# 每个线程里面都创建锁
mutex = threading.Lock()
# 上锁
mutex.acquire()
...这里编写代码能保证同一时刻只能有一个线程去操作, 对共享数据进行锁定...
# 释放锁
mutex.release()
死锁 (线程因为 多个线程互相调用对方的数据导致一直阻塞,造成程序的停滞响应)
一个程序运行后至少有一个进程,一个进程默认有一个线程,进程里面可以创建多个线程,线程是依附在进程里面的,没有进程就没有线程。
线程 进程对比:
进程优缺点:
- 优点:可以用多核
- 缺点:资源开销大
线程优缺点:
- 优点:资源开销小
- 缺点:不能使用多核
进程和线程都是完成多任务的一种方式
多进程要比多线程消耗的资源多,但是多进程开发比单进程多线程开发稳定性要强,某个进程挂掉不会影响其它进程。
多进程可以使用cpu的多核运行,所以与数据相关的操作使用进程,计算能力强。
多线程可以共享全局变量。
线程不能单独执行必须依附在进程里面
协程
协程,⼜称微线程,纤程,英⽂名Coroutine。是python个中另外⼀种实现多任务的⽅式,只不过⽐
线程更⼩占⽤更⼩执⾏单元。 为啥说它是⼀个执⾏单元,因为它⾃带CPU上下⽂。这样只要在合适的时
机, 我们可以把⼀个协程切换到另⼀个协程。 这个切换没有线程的切换开销。
协程的优点
- 比线程完成要节省资源
- 没有多线程的锁机制,因为只有一个线程,在协程中操作共享资源不加锁,所以执行效率比多线程高很多。
其原理是当一个任务函数遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的任务函数执行,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有任务函数在运行,而不是等待IO,得以实现多任务,提高程序执行效率。
安装:
pip3 install gevent
多个协程是依次执行的
import gevent
例子
import gevent
import time
import threading
from gevent import monkey
# 打补丁操作,让协程识别耗时操作(time.sleep, 网络请求, socket里面的接收数据, socket等待接受客户端的连接请求)
monkey.patch_all()
# 定义函数任务
def task(count):
for i in range(count):
print("当前遍历的数据为:", i)
# 获取当前协程
print("当前线程为:", gevent.getcurrent())
# 让协程执行任务时有耗时操作,协程会根据耗时操作切换到其它协程去执行
# 默认情况下,time.sleep 对于协程来说不是耗时操作
# time.sleep(0.2)
# 协程里面的耗时操作,会根据耗时操作切换到其它协程去执行
# gevent.sleep(0.2)
# 由于已经打上补丁,协程能够识别是耗时操作,遇到耗时操作可以切换到其它协程执行代码
time.sleep(0.2)
if __name__ == '__main__':
# 创建两个协程
g1 = gevent.spawn(task, 2)
g2 = gevent.spawn(task, 3)
# 默认情况下: 主线程不会等待协程执行结束再结束
# 解决办法:让当前线程等待协程执行结束
# g1.join()
# g2.join()
# 等待协程执行结束的第二种方式
# gevent.joinall([g1, g2])
# 验证使用gevent利用程完成多任务没有创建线程
result = threading.active_count()
print("当前进程的活动线程的数量为:", result)
# 扩展: 主线程有耗时操作,可以让协程切换执行
while True:
time.sleep(1)
## output
g = gevent.spawn(test, 1, 2, 3) # 参数直接放在后面
g.join()
扩展
多进程多线程存在的意义:
多线程:主要是解决了IO阻塞时,线程会切换,虽然切换线程也会消耗资源,但相⽐较等待
IO,节约了很多的时间。单核计算机时代,同时只能有⼀个线程执⾏,多线程的设计在IO操作
时,很提升效率。
多进程:现在的计算机都是多核计算机,多进程的设计提⾼了CPU的使⽤率,多个任务同时执行,加快了程序的效率。
进程和线程如何工作:
⼀个进程中可以有多个线程,每个进程都有⼀把GIL锁(GIL实际存在于C-Python解释器),
线程要想执⾏,就得先获取这把锁,拿到锁的线程开始执⾏,满⾜⼀定条件后,释放GIL锁,所
有线程⼜开始竞争这个GIL锁。
阻塞 非阻塞 同步 异步
- 同步:多个任务之间有先后顺序执⾏,⼀个执⾏完下个才能执行。(串行)
- 异步:多个任务之间没有先后顺序,可以同时执⾏,有时候⼀个任务可能要在必要的时候获取另⼀
个同时执行的任务的结果,这个就叫回调! - 阻塞:如果卡住了调用者,调⽤者不能继续往下执行,就是说调⽤者阻塞了。
- 非阻塞:如果不会卡住,可以继续执行,就是说非阻塞的
GIL 与 互斥锁
GIL锁,Global Interpreter Lock(全局解释器锁),它不是Python的特性,是 cpython解释器⾥的特性。就是线程在执⾏前,必须得获取到这个GIL锁,才能执⾏,也就是保证了进程中,同时只有⼀个线程在执⾏
GIL 存在的原因
- 单核计算机时代,计算机的算⼒有限,同⼀时刻只有⼀个线程执⾏,GIL的设计保证同时只有⼀个线程在运⾏且在IO操作时有效的提⾼了计算效率。
- 多核计算机时代,GIL锁却严重影响了Python的效率。多核情况下,单进程多线程,⽐单核情况下,效率差很多,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够⽆缝执⾏,但多核下,CPU0释放GIL后,其他CPU上的线程都会进⾏竞争,但GIL可能会⻢上⼜被
CPU0拿到,导致其他⼏个CPU上被唤醒后的线程会醒着等待到切换时间后⼜进⼊待调度状态,这样会造成线程颠簸(thrashing),导致效率更低 。 - python下想要充分利⽤多核CPU,就⽤多进程,原因是什么呢?原因是:每个进程有各⾃独⽴的
GIL,互不⼲扰,这样就可以真正意义上的并⾏执⾏,(多线程并不是真正意义上的并⾏执⾏,只能算
并发执⾏)所以在python中,多进程的执⾏效率优于多线程(仅仅针对多核CPU⽽⾔)。
GIL释放条件
- 线程执⾏完
- IO操作
- 阙值:Python2–ticks计数(100次操作);Python3–计时器(执⾏时间达到阈值后,当
前线程释放GIL
互斥锁
GIL锁存在的⽬的,就有保证了同⼀时刻只有⼀个线程在运⾏,⼀定程度上做到了线程安全。但不能完
全保证线程安全,因为多线程共享全局变量,在多个线程⼏乎同时操作全局变量是,可能会导致数据
紊乱(因为GIL锁的释放条件,⼀个线程可能还没有计算完,就被其他线程抢去执⾏),此时引⼊互斥
锁的,保证该线程执⾏完再让其他的线程执⾏。
协程
协程,⼜称微线程,纤程,英⽂名Coroutine。是python个中另外⼀种实现多任务的⽅式,只不过⽐
线程更⼩占⽤更⼩执⾏单元。 为啥说它是⼀个执⾏单元,因为它⾃带CPU上下⽂。这样只要在合适的时
机, 我们可以把⼀个协程切换到另⼀个协程。 这个切换没有线程的切换开销。