死锁现象、信号量、event事件、池的概念、协程
死锁现象
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
在使用互斥锁时就算掌握了如何抢,如何放,也会产生死锁现象。
比如:
from threading import Thread, Lock
import time
mutexA = Lock()
mutexB = Lock()
def f1(name, mutex1, mutex2):
mutex1.acquire()
print(f'{name}抢到了A锁')
mutex2.acquire()
print(f'{name}抢到了B锁')
mutex2.release()
print(f'{name}释放了B锁')
time.sleep(0.1)
mutex1.release()
print(f'{name}释放了A锁')
def f2(name, mutex1, mutex2):
mutex2.acquire()
print(f'{name}抢到了B锁')
mutex1.acquire()
print(f'{name}抢到了A锁')
mutex1.release()
print(f'{name}释放了A锁')
mutex2.release()
print(f'{name}释放了B锁')
if __name__ == '__main__':
# 创建多个线程
for i in range(5):
t1 = Thread(target=f1, args=(f'f1线程{i}', mutexA, mutexB))
t1.start()
for i in range(5):
t2 = Thread(target=f2, args=(f'f2线程{i}', mutexA, mutexB))
t2.start()
执行结果:
f1线程0抢到了A锁
f1线程0抢到了B锁
f1线程0释放了B锁
f2线程0抢到了B锁
f1线程0释放了A锁
f1线程1抢到了A锁
程序执行到一半直接卡住了,这个就是死锁现象,在上述例子中,执行f1的线程0释放了B锁后,执行f2的线程0就抢到了B锁,释放了A锁后,另一个执行f1的线程1就会抢到了A锁,抢到了A锁后他要抢B锁,但此时的B锁已经被抢了,所以在等待,而执行f2的线程0要抢A锁,也在等待,2个线程互相等待结果就造成了死锁。
信号量(了解)
在并发编程中信号量意思是就是多把互斥锁,如果说自定义互斥锁是单间,那么信号量就是宾馆。
Semaphore()方法就是用于创建信号量的。
from threading import Thread, Semaphore
import time
sp = Semaphore(5) # 创建5个互斥锁
def task(name):
sp.acquire() # 抢锁
print('%s正在占用' % name)
# 让执行结果更明显
time.sleep(2)
sp.release() # 放锁
if __name__ == '__main__':
for i in range(20):
t = Thread(target=task, args=(f'线程{i}',))
t.start()
此时的线程会5个5个的执行,因为只有有五把互斥锁可以抢。
event事件(了解)
子线程的运行可以由其他子线程决定,这个我们可以用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(5):
t = Thread(target=car, args=('AE86-%s' % i,))
t.start()
event.wait()就是等待的命令,event.set()就是让event.wait()取消等待,可以执行了。
进程池与线程池(重点)
学到这里,我们应该知道我们不可能无限制的创建进程和线程,如果无限制的创建进程和线程,会让计算机超过负荷,超载,并可能对计算机硬件造成损害。
为了避免计算机超过负荷,我们可以使用池,池的作用就是在保证计算机硬件安全的情况下提升程序的运行效率,进程池就是提前创建好一定数量的进程,后续反复并且只使用这些进程;线程池就是提前创建好一定数量的线程,后续反复并且只使用这些线程;如果任务超过了池里规定的数量,就会原地等待,所以池也相应的降低了程序的运行效率。
线程池演示
from concurrent.futures import ThreadPoolExecutor
import time
# 创建线程池
t_pool = ThreadPoolExecutor(15) # 提供15个线程,默认为cpu个数*5
'''上面的代码执行之后就会立刻创建15个等待工作的线程'''
def task(name):
print(f'线程{name}正在执行...')
time.sleep(2)
if __name__ == '__main__':
for i in range(90):
# 执行线程
t_pool.submit(task, i)
进程池演示
from concurrent.futures import ProcessPoolExecutor
import time
# 创建进程池
p_pool = ProcessPoolExecutor(3) # 提供3个进程,默认为cpu个数
'''上面的代码执行之后就会立刻创建3个等待工作的进程'''
def task(name):
print(f'进程{name}正在执行...')
time.sleep(2)
if __name__ == '__main__':
for i in range(15):
# 执行进程
p_pool.submit(task, i)
池在使用时是由返回值的,返回的是一个对象,需要调用result()才能取出,这里拿进程池举例:
from concurrent.futures import ProcessPoolExecutor
import time
# 创建进程池
p_pool = ProcessPoolExecutor(3) # 提供3个进程,默认为cpu个数
'''上面的代码执行之后就会立刻创建3个等待工作的进程'''
def task(name):
print(f'进程{name}正在执行...')
time.sleep(2)
return '返回值:%d' % name ** 2
if __name__ == '__main__':
for i in range(15):
# 执行进程,并获取返回值
res = p_pool.submit(task, i)
print(res.result())
但这时候的程序变成同步执行了,因为执行任务时需要等待返回值,所以我们可以使用add_done_callback(func)方法,只要调用add_done_callback(func)的对象有结果了,就会把返回的对象当成参数传入函数func中。
from concurrent.futures import ProcessPoolExecutor
import time
# 创建进程池
p_pool = ProcessPoolExecutor(3) # 提供3个进程,默认为cpu个数
'''上面的代码执行之后就会立刻创建3个等待工作的进程'''
def task(name):
print(f'进程{name}正在执行...')
time.sleep(2)
return '返回值:%d' % name ** 2
# 由于不知道有几个参数,使用可变形参
def func(*args, **kwargs):
# 从这里获取返回值
print(args[0].result())
if __name__ == '__main__':
for i in range(15):
# 执行进程,添加异步回调机制
p_pool.submit(task, i).add_done_callback(func)
"""
只要p_pool.submit(task, i)执行完返回一个对象
就会自动调用括号内的func函数处理
会把p_pool.submit(task, i)的返回的对象当成参数传入
"""
协程
协程就是自己通过代码来检测程序的IO操作并自己处理,让CPU感觉不到IO的存在从而最大幅度的占用CPU,简单的来说就是一个线程实现并发的效果。
基本使用
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, 'tom') # 第一个参数为任务,后面的参数为任务需要的参数
g2 = spawn(eat, 'tony') # 第一个参数为任务,后面的参数为任务需要的参数
g1.join() # 等待检测任务执行完毕
g2.join() # 等待检测任务执行完毕
print('运行时间:', time.time() - start_time)
# 输出结果:运行时间: 5.0176708698272705
主要方法:
- monkey;monkey.patch_all():固定搭配,用于检测所有的IO操作
- spawn():第一个参数为任务,后面的参数为任务需要的参数
- join():等待检测任务执行完毕
基于协程实现TCP服务端并发
服务端(Server)
import socket
from gevent import monkey;monkey.patch_all()
from gevent import spawn
def task(sock):
while True:
msg = sock.recv(1024)
print(msg.decode('utf8'))
sock.send(b'from server')
def run():
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
client, addr = server.accept()
spawn(task, client)
if __name__ == '__main__':
s = spawn(run)
s.join()
客户端(Client)
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
s = input('发送给服务端的消息:')
send_msg = 'from client: %s' % s
client.send(send_msg.encode('utf8'))
msg = client.recv(1024)
print(msg.decode('utf8'))