网络编程:死锁现象、信号量、event事件、进程池与线程池、协程
2022.4.21死锁现象、信号量、进程池、线程池、协程
- GIL与普通互斥锁的区别
- 验证多线程作用
- 死锁现象
- 信号量
- event事件
- 进程池与线程池
- 协程
一、GIL与普通互斥锁的区别
1、先代码验证GIL的存在
from threading import Thread
import time
money = 100
def task():
global money
money -= 1
for i in range(100): # 创建100个线程
t = Thread(target=task)
t.start()
print(money) # 0
分析:结果为0,说明各个线程抢到全局锁才回去执行,执行完再交接给下一位,最后都进行了数据修改
2、代码验证不同数据加不同锁
from threading import Thread, Lock
import time
money = 100
mutex = Lock() # 互斥锁
def task():
global money
mutex.acquire() # 抢锁
tmp = money
time.sleep(0.1)
money = tmp - 1
mutex.release() # 放锁
t_list = [] # 存放线程的列表
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join() # 给所有线程加join方法,确保所有的线程运行完毕
print(money) # 0
注意:如果这里没加互斥锁mutex的时候,结果为99,为什么?
分析:因为如果没有互斥锁保证它独立运行完再运行下一个的话,每个线程获取到的money都是100,tmp-1都是99,那么最终结果也就是99
"""
GIL是一个纯理论知识 在实际工作中根本无需考虑它的存在
GIL作用面很窄 仅限于解释器级别
后期我们要想保证数据的安全应该自定义互斥锁(使用别人封装好的工具)
"""
3、抢锁放锁简便写法
mutex = lock()
with mutex:
加锁的代码
二、验证多线程的作用
1、计算密集型多进程和多线程对比
from threading import Thread
from multiprocessing import Process
import os
import time
def work():
res = 1
for i in range(1, 100000):
res *= i
if __name__ == '__main__':
print(os.cpu_count()) # 8 查看当前计算机CPU个数
start_time = time.time()
# 多进程
p_list = []
for i in range(8):
p = Process(target=work)
p.start()
p_list.append(p)
for p in p_list:
p.join()
# # 多线程
# t_list = []
# for i in range(8):
# t = Thread(target=work)
# t.start()
# t_list.append(t)
# for t in t_list:
# t.join()
# print('总耗时:%s' % (time.time() - start_time)) # 总耗时:18.78506636619568
print('总耗时:%s' % (time.time() - start_time)) # 总耗时:4.876669883728027
对比进程和线程在我计算机上的表现,可以得出结论:
在多核计算机下,计算密集型任务使用多进程要比多线程更有优势
2、IO密集型多进程和多线程对比
直接复制上面代码,把work改成IO操作,进程线程各起100个:
def work():
time.sleep(2) # 模拟纯IO操作
...
# 多进程
总耗时:3.705044746398926
# 多线程
总耗时:2.0191071033477783
结论:IO密集型启用多线程比多进程效率高
三、死锁现象
ps:锁就算掌握了如何抢 如何放 也会产生死锁现象
代码演示:
from threading import Thread, Lock
import time
mutexA = Lock()
mutexB = Lock()
class MyThread(Thread):
def run(self):
self.f1()
self.f2()
def f1(self):
mutexA.acquire() # 抢A锁
print(f'{self.name}抢到了A锁')
mutexB.acquire() # 抢B锁
print(f'{self.name}抢到了B锁')
mutexB.release() # 放B锁
mutexA.release() # 放A锁
def f2(self):
mutexB.acquire() # 抢B锁
print(f'{self.name}抢到了B锁')
time.sleep(2)
mutexA.acquire() # 抢A锁
print(f'{self.name}抢到了A锁')
mutexA.release() # 放A锁
mutexB.release() # 放B锁
for i in range(10): # 创建10个线程
t = MyThread()
t.start()
# 结果:
Thread-1抢到了A锁
Thread-1抢到了B锁
Thread-1抢到了B锁
Thread-2抢到了A锁
# 然后就产生阻塞现象了,因为最后线程2抢到A锁然后取抢B锁时,B锁还在线程1手里,然而线程1下面也要抢A锁,两者都进入阻塞
结论:
锁不能轻易使用并且以后我们也不会在自己去处理锁都是用别人封装的工具
四、信号量
信号量在不同知识体系中,展现出来的功能时不一样的,
eg:
在并发编程中:信号量是把互斥锁
在djando框架中:信号量时达到某条件自动触发特定功能
代码演示:
"""
如果将自定义互斥锁比喻成是单个厕所(一个坑位)
那么信号量相当于是公共厕所(多个坑位)
"""
from threading import Thread, Semaphore
import time
import random
sp = Semaphore(5) # 创建一个有五个坑位(带门的)的公共厕所
def task(name):
sp.acquire() # 抢锁
print('%s正在蹲坑' % name)
time.sleep(random.randint(1, 5))
sp.release() # 放锁
for i in range(1, 31):
t = Thread(target=task, args=('伞兵%s号' % i, ))
t.start()
分析:上面代码创建带有五个位置的信号量,然后创建30个线程,会发现,代码结果会直接先运行5个线程,然后陆续运行接下来的线程,说明,信号量是设置好的5个位置,前面的位置腾出来后后面的线程再进去,以此类推
五、event事件(线程间通信)
作用:子线程的运行可以由其他子线程决定或者干涉
Event是线程间通信的机制之一:一个线程发送一个event信号,其他的线程则等待这个信号。常用在一个线程需要根据另外一个线程的状态来确定自己的下一步操作的情况。
Event原理:事件对象管理一个内部标志,通过set()方法将其设置为True,并使用clear()方法将其设置为False。wait()方法阻塞,直到标志为True。该标志初始为False。
代码验证:
from threading import Thread, Event
import time
event = Event() # 类似于造了一个红绿灯
def light():
print('红灯亮,原地等待')
time.sleep(3)
print('绿灯行,冲过马路')
event.set() # True
def car(name):
print('%s正在等红灯' % name)
event.wait() # 阻塞,等待为True时退出阻塞
print('%s加油门,开始飙车' % name)
t = Thread(target=light) # 创建1个线程light,模拟红绿灯
t.start()
for i in range(10): # 创建10个线程car,模拟汽车司机
t = Thread(target=car, args=('汽车司机%s' % i,))
t.start()
# 结果:
红灯亮,原地等待
汽车司机0正在等红灯
... # 省略
汽车司机9正在等红灯
绿灯行,冲过马路
汽车司机1加油门,开始飙车
... # 省略
汽车司机7加油门,开始飙车
# 拓展:
event.clear() # 将事件设置为False
is_set() # 当且仅当内部标志为True时返回True
可以看出,wait()造成阻塞,等待set()执行,然后结束阻塞
六、进程池与线程池(重点)
引入:
服务端必备的三要素
1.24小时不间断提供服务
2.固定的ip和port
3.支持高并发
回顾:
TCP服务端实现并发
多进程:来一个客户端就开一个进程(临时工)
多线程:来一个客户端就开一个线程(临时工)
问题:
计算机硬件是有物理极限的,我们不可能无限制的创建进程和线程
措施:
池:
保证计算机硬件安全的情况下提升程序的运行效率
进程池:
提前创建好固定数量的进程,后续反复使用这些进程(合同工)
线程池:
提前创建好固定数量的线程,后续反复使用这些线程(合同工)
强调:
如果任务超出了池子里面的最大进程或线程数,则原地等待
进程池和线程池其实降低了程序的运行效率,但是保证了硬件的安全!!!
代码演示(掌握):
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecytor
# 线程池
pool = ThreadPoolExecutor(5) # 线程池线程数默认时CPU个数的5倍,也可以自定义,这个代码执行之后就会立刻创建5个等待工作的线程
def task(n):
time.sleep(2)
print(n)
return '任务的执行结果:%s'%n**2
def func(*args, **kwargs):
# print(args, kwargs)
print(args[0].result())
for i in range(20):
# res = pool.submit(task, i) # 朝线程池中提交任务(异步)及参数
# print(res.result()) # 同步提交(获取任务的返回值)
'''这样效率太慢,不应该自己主动等待结果 应该让异步提交自动提醒>>>:异步回调机制'''
# 异步回调机制:异步提交自动提醒add_done_callback(func)
pool.submint(task, i).add_done_callback(func)
"""add_done_callback只要任务task有结果了,就会自动调用括号内的函数func处理,把task任务当作参数传入func"""
# 进程池
pool = ProcessPoolExecutor(5) # 进程池进程数默认是CPU个数 也可以自定义
'''上面的代码执行之后就会立刻创建五个等待工作的进程'''
pool.submit(task, i).add_done_callback(func)
七、协程
进程:资源单位
线程:执行单位
协程:单线程下实现并发
代码演示:
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') # 协程g1
g2 = spawn(eat, 'jason') # 协程g2
g1.join() # 等待检测任务执行完毕
g2.join() # 等待检测任务执行完毕
print('总耗时:', time.time() - start_time) # 5.00609827041626
基于协程实现TCP服务端并发
from gevent import monkey;monkey.patch_all()
from gevent import spawn
import socket
def communication(sock):
while True:
data = sock.recv(1024) # IO操作
print(data.decode('utf8'))
sock.send(data.upper())
def get_server():
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
sock, addr = server.accept() # IO操作
spawn(communication, sock)
g1 = spawn(get_server)
g1.join()
浙公网安备 33010602011771号