并发04--进/线程池、死锁与递归锁、信号量、事件、定时器、协程
1 死锁与递归锁(了解)
1.1 死锁(了解)
当你知道锁的使用抢锁必须要释放锁,其实 你在操作锁的时候也极其容易产生死锁现象(导致整个程序卡死 阻塞)
# 死锁:
是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,
若无外力作用,它们都将无法推进下去。
此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
from threading import Thread, Lock
import time
mutexA = Lock()
mutexB = Lock()
# 类的每次实例,都是不同的实例对象
# 若想实现类的每次实例,都是同一个实例对象,需要单例模式(要补)。*****
class Mythread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print('{} 抢到A锁'.format(self.name)) # 获取当前线程名
mutexB.acquire()
print('{} 抢到B锁'.format(self.name))
mutexB.release()
mutexA.release()
def func2(self):
mutexB.acquire()
print('{} 抢到B锁'.format(self.name))
time.sleep(2)
mutexA.acquire()
print('{} 抢到A锁'.format(self.name)) # 获取当前线程名
mutexA.release()
mutexB.release()
if __name__ == '__main__':
for i in range(10):
t = Mythread()
t.start()
'''
打印结果:
Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁
Thread-2 拿到A锁
然后就卡住,死锁了
'''
1.2 递归锁(了解)
# 递归锁的特点:
可以被连续的acquire和release
但是只能被第一个抢到这把锁执行上诉操作
内部有一个计数器,每acquire一次计数加 1,每release一次计数减1
只要计数不为0,那么其他人都无法抢到该锁
# 将普通锁换成递归锁
mutexA = Lock()
mutexB = Lock()
# 递归锁
mutexA = mutexB = RLock()
2 信号量(了解)
信号量在不同的阶段可能对应不同的技术点,在并发编程中信号量指的是锁!
# 互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据
如果我们将互斥锁比喻成一个厕所的话
那么信号量就相当于多个厕所----(同时有多个门,每个门都可以进入。跟多个进/线程,没有关系)
from threading import Thread, Semaphore
import time
import random
"""
利用random模块实现打印随机验证码(搜狗的一道笔试题):
实现思路:
1.使用random.randrange(10)获取0-9十个数字
2.小写字母对应ascii码中65-90
3.大写字母对应ascii码中97-122
4.通过chr(int)将ascii码转为字母
5.将步骤数字、大小写字母组成list,通过调用random.choice()随机选择数字、大小写字母
6.通过 for循环,进行6次选择,使用字符串拼接6次组成6位随机验证码
"""
sm = Semaphore(5) # 括号内写数字,代表开设几个坑位(默认为1)
def task(name):
sm.acquire()
print('{} 正在蹲坑'.format(name))
time.sleep(random.randint(1, 5))
sm.release()
print('{} 蹲坑结束'.format(name))
if __name__ == '__main__':
for i in range(20):
t = Thread(target=task, args=('伞兵{}'.format(i),))
t.start()
3 Event事件(了解)
一些进程/线程需要等待另外一些进程/线程运行完毕之后才能运行,类似于发射信号一样
# python线程的事件用于主线程控制其他线程的执行
# 事件主要提供了三个方法 set、wait、clear。
# 事件处理的机制:
全局定义了一个“Flag”
“Flag”值为True 则程序执行event.wait方法时,便不再阻塞; 反之,则阻塞
clear:将“Flag”设置为False
set :将“Flag”设置为True
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()
# 设置20辆车
for i in range(20):
i = Thread(target=car, args=('% s' % i,))
i.start()
4 线程-队列q(了解)
"""
同一个进程下多个线程数据是共享的,为什么同一个进程下还会去使用队列呢?
因为队列是管道+锁,所以用队列还是为了保证数据的安全
"""
import queue
# 我们现在使用的队列都是只能在本地测试使用,实际是用别人封装好的。
# 1.Queue 先进先出
q = queue.Queue(5)
q.put(1)
q.get()
q.get_nowait()
q.get(timeout=3)
q.full()
q.empty()
# 2.LifoQueue 后进先出
q = queue.LifoQueue()
q.put(1)
q.put(2)
q.put(3)
print(q.get()) # 3
# 优先级q 你可以给放入队列中的数据设置进出的优先级
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括号内放一个元祖 第一个放数字表示优先级
# 需要注意的是 数字越小优先级越高!!!
5 进程池与线程池 ******
先回顾之前TCP服务端实现并发的效果是怎么玩的?
每来一个人就开设一个进程或者线程去处理
# 前提:
无论是开设进程也好还是开设线程也好 是不是都需要消耗资源
只不过开设线程的消耗比开设进程的稍微小一点而已
我们是不可能做到无限制的开设进程和线程的 因为计算机硬件的资源更不上!!!
硬件的开发速度远远赶不上软件呐
我们的宗旨应该是在保证计算机硬件能够正常工作的情况下最大限度的利用它
# 池的目的
池是用来保证计算机硬件安全的情况下最大限度的利用计算机
它降低了程序的运行效率但是保证了计算机硬件的安全 从而让你写的程序能够正常运行
5.1 基本使用
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
# 1.介绍
concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor # 线程池,提供异步调用
ProcessPoolExecutor # 进程池,提供异步调用
# 2 基本方法
# 2.1 申明池
# 申明线程池
pool = ThreadPoolExecutor(5)
# 括号内可以传数字,就是固定数量的线程
池子造出来之后,里面会固定存在这五个线程,不会出现重复的创建与销毁的过程!
# 不传的话默认会开设当前计算机cpu个数的 5倍线程
# 申明进程池
pool = ProcessPoolExecutor(5)
# 括号内可以传数字 不传的话默认会开设当前计算机cpu个数进程
# 2.2 异步提交任务 (单个提交)
.submit(fn, *args, **kwargs)
# *args 给fn传递多个参数时,直接写参数名就行
eg: pool.submit(func1, a, b) # 就func1(a, b) 占一个进程
# pool.submit() 任务提交后返回的线程对象 才有下面的方法
# 2.2.1 直接获取结果
.result(timeout=None)
# 2.2.2 执行异步回调 (任务执行完成后,触发该函数)
.add_done_callback(fn) # fn: 回调函数
# 2.2.3 判断该线程对象 执行是否结束
.done()
# 2.3 批量异步提交任务 取代 for循环submit 的操作
.map(func, *iterables, timeout=None, chunksize=1)
# 给func传递多个参数时,*iterables可迭代对象
# 直接使用,多个参数的 可迭代对象
eg:
age_list = [18, 28, 38]
name_list = ['alex', 'egon', 'edmond']
pool.map(func, age_list, name_list)
# 2.4 关闭+等待执行
.shutdown(wait=True) # 相当于进程池的pool.close() + pool.join() 操作
wait = True # 等待池内所有任务执行完毕回收完资源后才继续 默认为True
wait = False # 立即返回,并不会等待池内的任务执行完毕
# 注:
1.不管wait参数为何值,整个程序都会等到所有任务执行完毕
2.submit和map必须在shutdown之前
# 最好使用 with上下文来管理 ***
eg:
with ProcessPoolExecutor(4) as executor:
executor.map(worker, [1, 2, 3, 4], [q, q, q, q])
# 任务的提交方式:
同步:提交任务后,原地等待任务的返回结果,期间不做任何事
异步:提交任务后,不等待任务的返回结果,执行继续往下执行
# 如何获取异步提交的返回结果? --通过回调机制 ***
1.for循环 + 任务.result()(了解)
先等到所有任务线程,执行完成
再循环所有完成后的任务, 调用任务.result(),获得返回值
2.回调函数
直接将获取返回结果,写在fn-回调函数内,
等任务线程异步提交后,各线程各自完成后,会自动触发回调函数执行,从而获取结果
# 案例
# 获取任务的返回结果:方式一 (了解)
def task(n):
print(n)
time.sleep(2)
return n ** 2
if __name__ == '__main__':
pool = ThreadPoolExecutor(5)
l_list = []
for i in range(20):
res = pool.submit(task, i) # 朝池子中提交20个任务 异步提交
l_list.append(res) # 将提交后的任务【线程对象】,添加到列表
# 等待池内所有任务执行完毕后,关闭线程池
pool.shutdown()
# 依次调用任务.result(),打印每次任务的返回结果
for l in l_list:
print(l.result())
print('主')
# 获取任务的返回结果:方式二 回调函数 (掌握)
def task(n):
print(n)
time.sleep(2)
return n ** 2
def call_back(res): # 参数res:提交后的任务【线程对象】
print('call back >>> {}'.format(res.result()))
if __name__ == '__main__':
pool = ThreadPoolExecutor(5)
for i in range(20):
res = pool.submit(task, i).add_done_callback(call_back)
print('主')
5.2 进程池中的进程间通信
# 强调: 上过大当
concurrent.futures模块 是高度封装的异步调用接口模块
使用里面的 进程池,则需要使用 高级通信类里 的队列
import random
import time
from multiprocessing import Manager # Manager()是一个进程间高级通信的类
from concurrent.futures import ProcessPoolExecutor
# 案例:主进程放了20个数,4个子进程去获取
def worker(worker_id, q):
while True:
try:
print(f'[{worker_id}] 获取到数据{q.get(timeout=2)}')
time.sleep(random.randint(1, 3))
q.task_done()
except Exception as e:
print(e)
break
if __name__ == '__main__':
q = Manager().Queue()
for i in range(20):
q.put(i)
with ProcessPoolExecutor(4) as executor:
executor.map(worker, [1, 2, 3, 4], [q, q, q, q])
print('working')
# 等待队列中所有数据被处理完
q.join()
print('All work completed')
5.3 总结
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# 开设进程池
pool = ProcessPoolExecutor(5)
# 异步提交,并执行回调
pool.submit(task, i).add_done_callback(call_back)
6 定时器
# 定时器,指定ns后执行某个任务
from threading import Timer
def test(name):
print('%s sb'%name)
t=Timer(1,test,args=('铁蛋',))
t.start()
7 协程
进程:资源单位
线程:执行单位
协程:这个概念完全是程序员自己意淫出来的 根本不存在
# 协程:单线程下实现并发
又称微线程,纤程。英文名Coroutine。
一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
# 协程的本质:
我们程序员自己再代码层面上检测我们所有的IO操作
一旦遇到IO了 我们在代码级别完成切换
这样给CPU的感觉是你这个程序一直在运行 没有IO
从而提升程序的运行效率
# 多道技术
切换+保存状态 # 并发的本质
CPU两种切换
1.程序遇到IO
2.程序长时间占用或者优先级更高的
# TCP服务端
accept (等待输入,IO操作) 协程的话,就会在这之间来回的切换
recv (等待输入,IO操作)
# 代码如何做到
切换+保存状态
切换:
切换不一定是提升效率 也有可能是降低效率
IO切 提升
没有IO切 降低
保存状态:
保存上一次我执行的状态 下一次来接着上一次的操作继续往后执行
yield (生成器中,挂起--保存状态的;next就取得上次状态的结果)
7.1 验证切换是否就一定提升效率(了解)
# 利用yield(保存状态)和next(获取状态执行一次)--验证切换是否就一定提升效率
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.3289263248443604
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)
7.2 gevent模块
# 但yield不能检测IO,实现遇到IO自动切换,故使用gevent模块
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程
在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。
Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
# 安装
pips install gevent
# 基本用法
# 创建一个协程对象g1 (任务函数名, 后面可以有多个参数 都是传给函数)
g1=gevent.spawn(func1,1,2,3,x=4,y=5)
g2=gevent.spawn(func2)
# 等待协程结束
g1.join() # 等待g1结束
g2.join() # 等待g2结束
# 或者上述两步合作一步:gevent.joinall([g1,g2])
# 获取func1的返回值
g1.value
from gevent import monkey;monkey.patch_all() # 猴子补丁
import time
from gevent import spawn # 检测IO操作
"""
gevent模块本身无法检测常见的一些io操作
在使用的时候需要你额外的导入一句话:
# 猴子补丁
from gevent import monkey
monkey.patch_all()
又由于上面的两句话在使用gevent模块的时候是肯定要导入的
所以还支持简写
from gevent import monkey;monkey.patch_all()
"""
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.055095434188843
g1 = spawn(heng) # 异步提交任务,故需要等待返回结果 .join()
g2 = spawn(ha)
# 再新加一个任务试试
g3 = spawn(heiheihei)
g1.join()
g2.join() # 等待被检测的任务执行完毕 再往后继续执行
# print(time.time() - start_time) # 3.021512031555176
g3.join()
print(time.time() - start_time) # 5.02057409286499
7.3 协程实现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()
总结
# 理想状态:
我们可以通过
多进程下面开设多线程
多线程下面再开设协程
从而使我们的程序执行效率提升
# 实际:利用封装好的框架来做