day 41 协程、死锁、池
1 死锁与递归锁
1.1 死锁
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
当你知道锁的使用抢锁必须要释放锁,其实你在操作锁的时候也极其容易产生死锁现象(整个程序卡死 阻塞)
点击查看代码
from threading import Thread, Lock
import time
mutexA = Lock()
mutexB = Lock()
# 类只要加括号多次 产生的肯定是不同的对象
# 如果你想要实现多次加括号等到的是相同的对象。使用单例模式
class MyThead(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print('%s 抢到A锁'% self.name) # 获取当前线程名
mutexB.acquire()
print('%s 抢到B锁'% self.name)
mutexB.release()
mutexA.release()
def func2(self):
mutexB.acquire()
print('%s 抢到B锁'% self.name)
time.sleep(2)
mutexA.acquire()
print('%s 抢到A锁'% self.name) # 获取当前线程名
mutexA.release()
mutexB.release()
if __name__ == '__main__':
for i in range(10):
t = MyThead()
t.start()
解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:
1.2 递归锁
案例一
"""
递归锁的特点
可以被连续的acquire和release,
但是只能被第一个抢到这把锁执行上述操作,
它的内部有一个计数器,每acquire一次计数加一,每realse一次计数减一,
只要计数不为0 那么其他人都无法抢到该锁。
"""
# 将上述的
mutexA = Lock()
mutexB = Lock()
# 换成
mutexA = mutexB = RLock()
案例二
from threading import RLock as Lock
import time
mutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()
2 信号量与事件
2.1 信号量
信号量在不同的阶段可能对应不同的技术点
在并发编程中信号量指的是锁!!!
如果我们将互斥锁比喻成一个厕所的话,那么信号量就相当于多个厕所
利用random模块实现打印随机验证码(搜狗的一道笔试题)
from threading import Thread, Semaphore
import time
import random
sm = Semaphore(5) # 括号内写数字 写几就表示开设几个坑位
def task(name):
sm.acquire()
print('%s 正在蹲坑'% name)
time.sleep(random.randint(1, 5))
sm.release()
if __name__ == '__main__':
for i in range(20):
t = Thread(target=task, args=('伞兵%s号'%i, ))
t.start()
2.2 Event事件
一些进程/线程需要等待另外一些进程/线程运行完毕之后才能运行,类似于发射信号一样
from threading import Thread, Event
import time
event = Event() # 造了一个红绿灯
def light():
print('红灯亮着的')
time.sleep(3)
print('绿灯亮了')
# 告诉等待红灯的人可以走了
event.set()
def car(name):
print('%s 车正在灯红灯'%name)
event.wait() # 等待别人给你发信号
print('%s 车加油门飙车走了'%name)
if __name__ == '__main__':
t = Thread(target=light)
t.start()
for i in range(20):
t = Thread(target=car, args=('%s'%i, ))
t.start()
3 线程q
queue队列 :使用import queue,用法与进程Queue一样
同一个进程下多个线程数据是共享的,为什么先同一个进程下还会去使用队列呢
因为队列是:管道 + 锁,所以用队列还是为了保证数据的安全
队列q ,先进先出
import queue
q = queue.Queue(3)
q.put(1)
q.put(2)
q.put(3)
q.get()
q.get_nowait()
q.get(timeout=3)
q.full()
q.empty()
后进先出q
import queue
q = queue.LifoQueue(3) # last in first out
q.put(1)
q.put(2)
q.put(3)
print(q.get()) # 3
优先级q,你可以给放入队列中的数据设置进出的优先级
import queue
q = queue.PriorityQueue(4)
q.put((10, '111'))
q.put((100, '222'))
q.put((0, '333'))
q.put((-5, '444'))
print(q.get()) # (-5, '444')
# put括号内放一个元祖 第一个放数字表示优先级
# 需要注意的是 数字越小优先级越高!!!
4 进程池与线程池(掌握)
先回顾之前TCP服务端实现并发的效果是怎么玩的,每来一个人就开设一个进程或者线程去处理
无论是开设进程也好还是开设线程也好,都需要消耗资源,只不过开设线程的消耗比开设进程的稍微小一点而已
我们是不可能做到无限制的开设进程和线程的,因为计算机硬件的资源更不上!!!
硬件的开发速度远远赶不上软件呐
我们的宗旨应该是在保证计算机硬件能够正常工作的情况下最大限度的利用它
4.1 池的概念
什么是池?
池是用来保证计算机硬件安全的情况下最大限度的利用计算机
它降低了程序的运行效率但是保证了计算机硬件的安全,从而让你写的程序能够正常运行
4.2 基本使用
池子造出来之后 ,面会固定存在五个线程,这个五个线程不会出现重复创建和销毁的过程
池子的使用非常的简单:pool.submit(task, 1) ,朝池子中提交任务,异步提交,你只需要将需要做的任务往池子中提交即可,自动会有人来服务你。res.result() 拿到的就是异步提交的任务的返回结果
任务的提交方式:
同步:提交任务之后原地等待任务的返回结果 期间不做任何事
异步:提交任务之后不等待任务的返回结果 执行继续往下执行
返回结果如何获取:异步提交任务的返回结果,应该通过回调机制来获取
回调机制:就相当于给每个异步任务绑定了一个定时炸弹,一旦该任务有结果立刻触发爆炸
示例1
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
import os
pool = ThreadPoolExecutor(5) # 池子里面固定只有五个线程
# 括号内可以传数字,不传的话默认会开设当前计算机cpu个数五倍的线程
def task(n):
print(n,os.getpid())
time.sleep(2)
return n**n
if __name__ == '__main__':
print('主')
t_list = []
for i in range(20): # 朝池子中提交20个任务
res = pool.submit(task, i) # <Future at 0x100f97b38 state=running>
t_list.append(res)
# 关闭线程池。等待线程池中所有的任务执行完毕之后再继续往下执行
pool.shutdown()
for t in t_list:
print('>>>:',t.result()) # 线程执行结果的排序肯定是有序的。res.result() 拿到的就是异步提交的任务的返回结果
示例2
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
import os
pool = ThreadPoolExecutor(5)
def task(n):
print(n,os.getpid())
time.sleep(2)
return n**n
def call_back(n):
print('call_back>>>:',n.result())
if __name__ == '__main__':
for i in range(20): # 朝池子中提交20个任务
res = pool.submit(task, i).add_done_callback(call_back)
# 回调机制返回的还是一个对象,这个对象被当做参数传给了call_back,之后得到真正的结果。这样就实现了线程一结束就拿到结果
总结
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
pool = ProcessPoolExecutor(5)
pool.submit(task, i).add_done_callback(call_back)
点击查看代码
"""
硬件的发展肯定是赶不上软件的开发速度的
思考我们以前借助于开设进程和线程的方式来实现TCP服务端的并发
每来一个客户端就开设一个进程或者线程
无论是开设进程还是开设线程其实都需要消耗一定的资源
我们应该在保证计算机硬件安全的情况下,最大限度的利用计算机
池的概念
它的出现是为了保证计算机硬件的安全
降低了程序的运行效率 但是保证了计算机硬件安全
"""
# 进程池线程池都不需要我们自己去造 直接使用封装好的模块
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
# 1 生成进程池线程池
pool1 = ThreadPoolExecutor() # 不填默认是cpu个数的五倍
pool2 = ProcessPoolExecutor() # 不填默认就是cpu的个数
# 2 朝池子中提交任务
pool1.submit(task,args...) # 异步提交
# 3 submit其实会返回一个Future类的对象 该对象调用result就能获取到任务的结果
res = pool1.submit(task,args...)
res.result() # 同步
# 4 池子对象的方法
pool1.shotdown() # 关闭池子 等待池子中所有的任务运行结束 再继续往后执行代码
# 5 异步回调机制
"""给每一个异步提交的任务绑定一个方法,一旦任务有结果了会立刻自动触发该方法"""
pool1.submit(task,args).add_done_callback(call_back)
# 注意异步回调函数拿到的也是一个对象
5 协程
5.1 介绍
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:
#1. 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。
#2. 作为1的补充:可以检测io操作,在遇到io操作的情况下才发生切换
点击查看代码
进程:资源单位
线程:执行单位
协程:这个概念完全是程序员自己意淫出来的,根本不存在,
单线程下实现并发,
我们程序员自己在代码层面上检测我们所有的IO操作,
一旦遇到IO了,我们在代码级别完成切换,
这样给CPU的感觉是你这个程序一直在运行,没有IO,
从而提升程序的运行效率。
------------------------------------------------------
多道技术
切换+保存状态
CPU两种切换
1.程序遇到IO
2.程序长时间占用
TCP服务端
accept
recv
------------------------------------------------------
代码如何做到
切换+保存状态
切换
切换不一定是提升效率,也有可能是降低效率
在有IO时进行切换>>>>>>>>>>提升效率
没有IO时,即纯计算时切换>>>降低效率
保存状态
保存上一次我执行的状态 下一次来接着上一次的操作继续往后执行
yield
-----------------------------------------------------------------------------
单线程下实现并发
这个概念完全是我们程序员自己想出来
多道技术
切换+保存状态
我们想通过代码层面自己检测IO行为。一旦遇到IO代码层面实现切换
这样给操作系统的感觉好像我这个程序一直运行没有IO
欺骗操作系统从而最大化的利用CPU
一味的切换加保存状态也有可能会降低程序的效率
计算密集型的 不行
IO密集型的 可以
需要强调的是:
#1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
#2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
对比操作系统控制线程的切换,用户在单线程内控制协程的切换
优点如下:
#1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
#2. 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点如下:
#1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
#2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
5.2 验证切换是否就一定提升效率
点击查看代码
import time
# 串行执行计算密集型的任务 1.2372429370880127
def func1():
for i in range(10000000):
i + 1
def func2():
for i in range(10000000):
i + 1
start_time = time.time()
func1()
func2()
print(time.time() - start_time)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#切换 + yield 2.1247239112854004。由此可见在没有IO,纯计算时进行切换降低了效率
import time
def func1():
while True:
10000000 + 1
yield
def func2():
g = func1() # 先初始化出生成器
for i in range(10000000):
i + 1
next(g)
start_time = time.time()
func2()
print(time.time() - start_time)
5.3 gevent模块(select机制)
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,
安装
pip3 install gevent
基本使用
g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
g2=gevent.spawn(func2)
g1.join() #等待g1结束
g2.join() #等待g2结束
#或者上述两步合作一步:gevent.joinall([g1,g2])
g1.value#拿到func1的返回值
案例
点击查看代码
"""
gevent模块本身无法检测常见的一些io操作,所以在使用的时候需要你额外的导入一句话:
from gevent import monkey
monkey.patch_all()
又由于上面的两句话在使用gevent模块的时候是肯定要导入的,所以还支持简写:
from gevent import monkey;monkey.patch_all()
"""
------------------------------------------------------------------------------------------
from gevent import monkey;monkey.patch_all()
from gevent import spawn
import time
def heng():
print('哼')
time.sleep(2)
print('哼')
def ha():
print('哈')
time.sleep(3)
print('哈')
def heiheihei():
print('heiheihei')
time.sleep(5)
print('heiheihei')
start_time = time.time()
heng()
ha()
print(time.time() - start_time) # 单线程下 5.005702018737793
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
spawn(heng) # heng不要加括号,spawn会默认运行,spawn(heng)有一个返回值g.start()
spawn(ha)
print(time.time() - start_time) # 0,这是因为这里spawn为异步提交,提交后主进程直接来到最后一行
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
g1 = spawn(heng)
g2 = spawn(ha)
g3 = spawn(heiheihei)
g1.join()
g2.join() # 等待被检测的任务执行完毕 再往后继续执行
g3.join()
print(time.time() - start_time) # 5.005702018737793
5.4 协程实现TCP服务端的并发
点击查看代码
# 服务端
from gevent import monkey;monkey.patch_all()
import socket
from gevent import spawn
def communication(conn):
while True:
try:
data = conn.recv(1024)
if len(data) == 0:
break
conn.send(data.upper())
except ConnectionResetError as e:
print(e)
break
conn.close()
def server(ip, port):
server = socket.socket()
server.bind((ip, port))
server.listen(5)
while True:
conn, addr = server.accept()
spawn(communication, conn)
if __name__ == '__main__':
g1 = spawn(server, '127.0.0.1', 8080)
g1.join()
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# 客户端
from threading import Thread, current_thread
import socket
def x_client():
client = socket.socket()
client.connect(('127.0.0.1',8080))
n = 0
while True:
msg = '%s say hello %s'%(current_thread().name,n)
n += 1
client.send(msg.encode('utf-8'))
data = client.recv(1024)
print(data.decode('utf-8'))
if __name__ == '__main__':
for i in range(500):
t = Thread(target=x_client)
t.start()
总结
"""
理想状态:
我们可以通过
多进程下面开设多线程
多线程下面再开设协程序
从而使我们的程序执行效率提升
"""