GIL与普通互斥锁区别
GIL锁和互斥锁的异同点
相同:
都是为了解决解释器中多个线程资源竞争的问题
异:
1.互斥锁是Python代码层面的锁,解决Python程序中多线程共享资源的问题(线程数据共共享,当各个线程访问数据资源时会出现竞争状态,造成数据混乱);
2.GIL是Python解释层面的锁,解决解释器中多个线程的竞争资源问题(多个子线程在系统资源竞争是,都在等待对象某个部分资源解除占用状态,结果谁也不愿意先解锁,然后互相等着,程序无法执行下去)。
GIL对程序的影响:
1.Python中同一时刻有且只有一个线程会执行;
2.Python中的多个线程由于GIL锁的存在无法利用多核CPU;
3.Python中的多线程不适合计算机密集型的程序;
4.如果程序需要大量的计算,利用多核CPU资源,可以使用多进程来解决。
IO密集型和计算密集型(CPU密集型)
计算密集型和IO密集型的区别
IO 密集型:系统运作,大部分的状况是CPU 在等I/O (硬盘/内存)的读/写。
CPU 密集型(计算密集型):大部份时间用来做计算、逻辑判断等CPU 动作的程序称之CPU 密集型(计算密集型)。
(CPU密集型)计算密集型任务的特点:
要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。
这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任
务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率
很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
IO密集型任务的特点:
涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在
等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越
高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言
替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是
开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
主要使用场景:
多进程适合在CPU 密集型操作(cpu 操作指令比较多,如位数多的浮点运算)。
多线程适合在IO 密集型操作(读写数据操作较多的,比如爬虫)。
单个CPU
多个IO密集型任务
多进程:浪费资源 无法利用多个CPU
多线程:节省资源 切换+保存状态
多个计算密集型任务
多进程:耗时更长 创建进程的消耗+切换消耗
多线程:耗时较短 切换消耗
多个CPU
多个IO密集型任务
多进程:浪费资源 多个CPU无用武之地
多线程:节省资源 切换+保存状态
多个计算密集型任务
多进程:利用多核 速度更快
多线程:速度较慢
死锁现象
开发过程中使用线程,在线程间共享多个资源的时候,
如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但一旦发生就会造成应用的停止响应,程序不做任何事情。
避免死锁:
解决:
1.重构代码
2.添加超时释放锁
添加超时释放锁
from threading import Thread,Lock
import time
lockA = Lock()
lockB = Lock()
#自定义线程
class MyThread1(Thread):
#不论进程还是线程重写的都是run方法
def run(self):
if lockA.acquire():#如果可以获取到锁则返回True
print(self.name +'A锁')
time.sleep(0.1)
if lockB.acquire(timeout=3):#在acquire函数中阻塞,一直等待锁,不能往下运行了
#如果加上超时则表示退出acquire,继续往下执行把a锁释放了
print(self.name +"A锁+B锁")
lockB.release()
lockA.release()
#自定义线程
class MyThread2(Thread):
#不论进程还是线程重写的都是run方法
def run(self):
if lockB.acquire():#如果可以获取到锁则返回True
print(self.name +'B锁')
time.sleep(0.1)
if lockA.acquire(timeout=3):
print(self.name +"A锁+B锁")
lockA.release()
lockB.release()
def main():
pass
if __name__ == "__main__":
#若不加上超时,则会一直不能进入A锁+B锁情况。
#造成线程1,2一直死等
MyThread1().start()
MyThread2().start()
信号量
信号量:是最古老的同步原语之一,是一个计数器
当资源释放时计数器就会递增,当资源申请时计数器就会递减。可以认为信号量就代表着资源是否可用
以停车场的运作为例。
假设停车场只有三个车位,开始三个车位都是空的。这时同时来了五辆车,看门人开闸允许其中三辆直
接进入,剩下的车则必须在入口等待,后续来的车也在入口处等待。这时一辆车想离开停车场,告知看门人,
打开闸门放他出去,看门人看了看空车位数量,然后看门人才让外面的一辆车进去。如果又离开两辆,则又可
以放入两辆,如此往复。
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,
看门人起的就是信号量的作用。
python里面的信号量semaphore
python统一了所有的命名,使用与线程锁(互斥锁)同样的方法命名消耗和释放资源
acquire方法 消耗资源加1 空车位减1
release方法 释放资源加1 空车位加1
创建Semaphore类实例才可以使用信号量semaphore
通过该类的构造方法传入计数器的最大值空车位总数
from threading import Semaphore
Max =3
s =Semaphore(Max)
print(s._value)#输出计数器的值
s.acquire()#消耗1个资源
s.acquire()
s.acquire()
print(s._value)#输出计数器的值 这时输出的是0 也可以表示为False 当设定条件时,可以使用False来作为条件判断
# s.acquire()#已经没有资源了,再减就一直处于等待状态,除非设定了其他资源在执行完毕后并释放 这样才能继续消耗
s.release()#释放1个资源
s.release()
s.release()
# s.release()#已超过资源设定的最大值了,再加就抛出异常 相当于停车场一共才3个车位,怎么会显示有4个车位呢?
print(s._value)#输出计数器的值
event事件
"""
子线程的运行可以由其他子线程决定!!!
"""
Event几种方法:
event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。
例子:红绿灯
from threading import Thread,Event
import time
event=Event() # 创建一个红绿灯
def light():
print('红灯正亮着')
time.sleep(3)
event.set() #绿灯亮
def car(name):
print('车%s正在等绿灯' %name)
event.wait() #等灯绿 此时event为False,直到event.set()将其值设置为True,才会继续运行.
print('车%s通行' %name)
if __name__ == '__main__':
# 红绿灯
t1=Thread(target=light)
t1.start()
# 车
for i in range(10):
t=Thread(target=car,args=(i,))
t.start()
进程池与线程池
池的概念
池是用来保证计算机硬件安全的情况下最大限度的利用计算机
它降低了程序的运行效率但是保证了计算机硬件的安全从而让你写的程序能够正常运行
'''
无论是开设进程也好还是开设线程也好 都需要消耗资源
只不过开设线程的消耗比开设进程的稍微小一点而已
我们是不可能做到无限制的开设进程和线程的 因为计算机硬件的资源更不上!!!
硬件的开发速度远远赶不上软件
我们的宗旨应该是在保证计算机硬件能够正常工作的情况下最大限度的利用它
'''
进程池/线程池类方法
'''
进程池和线程池的使用方法完全一致,只是它们的类名不同而已,另外要注意区分任务类型,对于IO多计算
少的任务使用线程池,对于计算多IO少的任务使用进程池。
'''
concurrent.futures模块是CPython官方提供的进程池/线程池模块,ThreadPoolExecutor类封装了线程
池的相关方法、ProcessPoolExecutor类封装了进程池的相关方法。 以下三个是进程池类和线程池类共有的
方法,用法完全一致:
----------------------------------------------------------------------------------------
submit方法,submit(fn, /, *args, **kwargs):
功能:
调度可调用对象fn,以 fn(*args **kwargs)方式执行并返回Future实例(它可以返回调用对象fn返回的结果)。返回Future类的实例。
参数:
fn:可调用对象。
/:指定后续的args不能以名字传参,只能通过位置传参。
*args:使用args变量接收用户传的所有位置参数。
**kwargs:使用kwargs变量接收用户传的所有关键字参数。
----------------------------------------------------------------------------------------
map方法,map(func, *iterables, timeout=None, chunksize=1):
功能:
调度可调用对象func,以 func(遍历iterables中的元素)方式同时执行并返回func的结果到迭代器。
返回迭代器,遍历该迭代器可得到所以func的返回值。
参数:
func:指可调用对象。
iterables:指可迭代对象,map会遍历iterables并使用它的每一项元素作为func的参数提交任务。
timeout:指当遍历iterables时延迟了timeout秒后仍然未获取到内容时抛
concurrent.futures.TimeoutError异常, timeout可为int或float类型。 若timeout未指定或为None,则不限制等待时间。
chunksize:仅当使用进程池ProcessPoolExecutor时有效,该参数必须是正整数,
用来指定将iterables分割成几份提交任务。默认值是
1表示不分割,当任务数量很多时分割提交任务可以显著提高性能。该参数是3.5版以后新增的。
----------------------------------------------------------------------------------------
shutdown方法,shutdown(wait=True, *, cancel_futures=False):
功能:
用来关闭进程/线程池。
如果使用with语句,你就可以避免显式调用这个方法,它会在全部任务执行完毕或异常退出时自动调用
shutdown(wait=True)。
参数:
wait:用来指定何时关闭进程/线程池。若wait为True则在所有任务执行完毕后才关闭进程/线程池,默
认值是True;若wait为False则不再接收后续任务,待正在执行的任务完成后立即关闭进程/线程池。
cancel_futures:用来指定是否取消尚未开始的任务。True表示取消;False表示不取消,默认值为
False。该参数是3.9版以后新增的。
线程池,使用submit方式提交任务:
from threading import current_thread
import time, random
from concurrent.futures import ThreadPoolExecutor # 导入线程池类
def func(n):
print(current_thread().ident, f'任务“{n}”开始运行!')
time.sleep(random.randint(1, 3))
print(current_thread().ident, f'任务“{n}”运行完毕。')
if __name__ == '__main__': # win平台下必须要加,linux和mac平台下可以不加。
pool = ThreadPoolExecutor(3) # 创建包含3条线程的线程池
for i in range(9):
pool.submit(func, i) # 提交任务
进程池,使用submit方式提交任务:
from multiprocessing import current_process
import time, random
from concurrent.futures import ProcessPoolExecutor # 导入进程池类
def func(n):
print(current_process().ident, f'任务“{n}”开始运行!')
time.sleep(random.randint(1, 3))
print(current_process().ident, f'任务“{n}”运行完毕。')
if __name__ == '__main__': # win平台下必须要加,linux和mac平台下可以不加。
pool = ProcessPoolExecutor(3) # 创建包含3条进程的进程池
for i in range(9):
pool.submit(func, i) # 提交任务
回调方法(进程和线程方法一样)
from concurrent.futures import ThreadPoolExecutor
import time, random
def pow2(n):
time.sleep(random.uniform(1, 4)) # 随机休息1至4秒,方便看清多线程运行效果。
return n, n ** 2
def print_pow2(fut): # 打印pow2处理结果的回调函数
print(f'{fut.result()[0]}的运算结果是{fut.result()[1]}')
pool = ThreadPoolExecutor(3) # 开启3线程执行任务
for i in range(7): #
pool.submit(pow2, i).add_done_callback(print_pow2) # 线程池执行后返回future实例
'''
ps:使用add_done_callback方法绑定回调函数print_pow2,在pow2返回结果后print_pow2立即被调
用,print_pow2只能接收一个参数即future实例,使用该实例的result方法可以得到pow2的返回结果。
使用回调方法的好处是多线程并发执行时任何一条线程运行完毕后都可立即用回调函数对它的返回值进行处
理。如果不用回调方法那么只能等到全部任务运行结束后才能对返回值进行处理。
'''
协程
协程是协调个部分代码达到资源最大利用,这才是真正的协程,在协程中要有任务的安排调整。
协程就是这样发生在一个可能发生长时间阻塞的地方,我们不是让CPU做无用的等待,而是让CPU在等待的
时间干点其他有用的事情,我们手动进行任务切换的过程就是协程。
gevent的介绍
greenlet已经实现了协程,但是这个还要人工切换,这里介绍一个比greenlet更强大而且能够自动切换
任务的第三方库,那就是gevent。
gevent内部封装的greenlet,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比
如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当
的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有
greenlet在运行,而不是等待IO。
安装gevent
pip3 install gevent
给程序打补丁
from gevent import monkey
# 打补丁,让gevent框架识别耗时操作,比如:time.sleep,网络请求延时
monkey.patch_all()
实现
from gevent import monkey;monkey.patch_all() # 固定编写 用于检测所有的IO操作
from gevent import spawn
import time
def play(name):
print('%s play 1' % name)
time.sleep(5)
print('%s play 2' % name)
def eat(name):
print('%s eat 1' % name)
time.sleep(3)
print('%s eat 2' % name)
start_time = time.time()
g1 = spawn(play, 'jason')
g2 = spawn(eat, 'jason')
g1.join() # 等待检测任务执行完毕
g2.join() # 等待检测任务执行完毕
print('总耗时:', time.time() - start_time) # 正常串行肯定是8s+