FRI.死锁及协程
死锁与递归锁
什么是死锁
- 所谓死锁就是两个或两个以上进程或线程在执行过程中,因争夺资源而造成一种互相等待的现象,若无外力作用,他们都将无法推进下去
import time
from threading import Thread, RLock
lock1 = Lock()
lock2 = Lock()
def f1(name):
lock1.acquire()
print("%s 抢到了炮弹" % name)
time.sleep(1)
lock2.acquire()
print("%s 抢到了意大利炮" % name)
print("%s 开始攻城" % name)
time.sleep(1)
lock2.release()
print("打完了")
lock1.release()
def f2(name):
lock2.acquire()
print("%s 抢到了意大利炮" % name)
time.sleep(1)
lock1.acquire()
print("%s 抢到了炮弹" % name)
print("%s 开始攻城" % name)
time.sleep(1)
lock1.release()
print("打完了")
lock2.release()
if __name__ == '__main__':
for name in ['一营长', '二营长', '三营长']:
t1 = Thread(target=f1, args=(name,))
t1.start()
for name in ['一排长', '二排长', '三排长']:
t2 = Thread(target=f2, args=(name,))
t2.start()
递归锁解决死锁问题
import time
# RLock, 递归锁,可重入锁
from threading import Thread, RLock
lock1 = Lock()
lock2 = Lock1
def f1(name):
lock1.acquire()
print("%s 抢到了炮弹" % name)
time.sleep(1)
lock2.acquire()
time.sleep(3)
print("%s 抢到了意大利炮" % name)
print("%s 开始攻城" % name)
time.sleep(1)
lock2.release()
print("打完了")
lock1.release()
def f2(name):
lock2.acquire()
print("%s 抢到了意大利炮" % name)
time.sleep(1)
lock1.acquire()
print("%s 抢到了炮弹" % name)
print("%s 开始攻城" % name)
time.sleep(1)
lock1.release()
print("打完了")
lock2.release()
if __name__ == '__main__':
for name in ['一营长', '二营长', '三营长']:
t1 = Thread(target=f1, args=(name,))
t1.start()
for name in ['一排长', '二排长', '三排长']:
t2 = Thread(target=f2, args=(name,))
t2.start()
- 典型问题:科学家吃面
线程 Queue
- 线程 Queue 的主要目的是为了保证数据的安全
队列先进先出
class queue.Queue(maxsize=0)
import queue
q=queue.Queue()
q.put('first')
q.put('second')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
'''
结果(先进先出):
first
second
third
'''
栈先进后出
class queue.LifoQueue(maxsize=0)
import queue
q=queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
'''
结果(先进后出):
third
second
first
'''
优先级队列
import queue
q=queue.PriorityQueue()
#put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))
print(q.get())
print(q.get())
print(q.get())
'''
结果(数字越小优先级越高,优先级高的优先出队):
(10, 'b')
(20, 'a')
(30, 'c')
'''
进程池和线程池
concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor:线程池,提供异步调用
ProcessPoolExecutor:进程池,提供异步调用
基本方法
submit(fn, *args, **kwargs):异步提交任务
map(func, *iterables, timeout=None, chunksize=1):取代for循环submit的操作
shutdown(wait=True):相当于进程池的
pool.close()+pool.join()操作
- wait=True,等待池内所有任务执行完毕回收完资源后才继续
- wait=False,立即返回,并不会等待池内的任务执行完毕
- 但不管wait参数为何值,整个程序都会等到所有任务执行完毕
- submit和map必须在shutdown之前
result(timeout=None):取得结果
add_done_callback(fn):回调函数
import time
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
def task():
print("这是子进程")
time.sleep(2)
if __name__ == '__main__':
p_pool= ProcessPoolExecutor()
p_pool.submit(task)
p_pool.shutdown() # 相当于 join 方法
print("这是主进程")
回调函数
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time
def task(n):
time.sleep(2)
# print("我是子进程")
return n+n
def call_back(res):
print(res.result())
if __name__ == '__main__':
urls = [1, 2, 3,4]
p = ThreadPoolExecutor(3) # 里面有3个进程
for url in urls:
p.submit(task, url).add_done_callback(call_back)
协程
- 协程即是单线程下的并发,又称微线程(Coroutine)。协程是一种用户态的轻量级线程,即协程是用户程序自己控制调度的。
注意
-
Python 的线程属于内核级别,即由操作系统控制调度(若单线程遇到 IO 或执行时间过长就会被迫交出 CPU 执行权限,切换其他线程运行)
-
单线程内开启协程,一旦遇到 IO,就会从**应用程序级别(非控制系统)**控制切换,以此来提升效率(!!!非IO操作的切换与效率无关)
对比操作系统控制线程的切换,用户在单线程内控制协程的切换
协程的优点
1.协程的切换占用的资源更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级。
2.单线程内就可以实现并发的效果,最大限度地利用 CPU。
协程的缺点
1.协程的本质是单线程下,无法利用多核,可以使一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程。
2.协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
协程的特点
1.必须在只有一个单线程里实现并发
2.需要改共享数据而不加锁
3.用户程序里自己保存多个控制流的上下文栈
4.附加:一个协程遇到 IO 操作自动切换到其他协程(通过 gevent 模块的 select 机制实现检测 IO)
Greenlet模块实现状态切换
from greenlet import greenlet
def eat(name):
print("吃了一口")
g2.switch(name)
print("又吃了一口")
g2.switch()
def play(name):
print("玩了一下")
g1.switch()
print("又玩了一下")
if __name__ == '__main__':
g1 = greenlet(eat)
g2 = greenlet(play)
g1.switch("ly") # 可以在第一次switch时传入参数,以后都不需要
- greenlet只是提供了一种比教便捷的切换方式,但当切到一个任务执行时如果遇到IO,依然会原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。
- 单纯的切换(在没有 IO 的情况下或者没有重复开辟内存空间的操作),反而会降低程序的运行速度
gevent 实现 IO 切换
import gevent
def eat(name):
print("吃了一口")
gevent.sleep(2)
print("又吃了一口")
def play(name):
print("玩了一下")
gevent.sleep(2)
print("又玩了一下")
if __name__ == '__main__':
g1 = gevent.spawn(eat, name='ly')
g2 = gevent.spawn(play, name='ly')
g1.join()
g2.join()
猴子补丁
- 这个词原来为Guerrilla Patch,杂牌军、游击队,说明这部分不是原装的,在英文里guerilla发音和gorllia(猩猩)相似,再后来就写了monkey(猴子)。
- 还有一种解释是说由于这种方式将原来的代码弄乱了(messing with it),在英文里叫monkeying about(顽皮的),所以叫做Monkey Patch。
class Monkey():
def play(self):
print('猴子在玩')
class Dog():
def play(self):
print('狗子在玩')
m=Monkey()
m.play()
m.play=Dog().play
m.play()
# 类似于 property 装饰器
monkey patch的应用场景
- 很多用到 import json, 后来发现 ujson 性能更高,如果觉得把每个文件的 import json 改成 import ujson as json 成本较高, 或者说想测试一下 ujson 替换是否符合预期, 只需要在入口加上:
import json
import ujson
def monkey_patch_json():
json.__name__ = 'ujson'
json.dumps = ujson.dumps
json.loads = ujson.loads
monkey_patch_json()
aa=json.dumps({'name':'lqz','age':19})
print(aa)