并发编程(四)
GIL与普通互斥锁区别
# 1.先验证GIL的存在
from threading import Thread
data = 5
def num(i):
global data
data -= 1
print(f'第{i}次取值结果:{data}')
for i in range(1,6):
t = Thread(target=num,args=(i,))
t.start()
运行结果:
第1次取值结果:4
第2次取值结果:3
第3次取值结果:2
第4次取值结果:1
第5次取值结果:0
'''
联想昨天的内容:
两个厨师线程一起做饭,饭做好的顺序会有颠倒,说明线程运行运行有快有慢,没有固定顺序
但是在操作数据时却是按照顺序取出,说明python的数据存在GIL锁
'''
# 2.再验证不同数据加不同锁
from threading import Thread,Lock
# mutex = Lock()
data = 5
t_list = []
def num():
global data
# mutex.acquire() # 抢锁
a = data
time.sleep(0.1)
data = a-1
# mutex.release() # 放锁
for i in range(5):
t = Thread(target=num)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(data)
运行结果:
4
加锁后运行结果:
0
'''
GIL锁只用于对解释器级别的数据修改,当修改赋值方式后,出现无五个线程取的data都是5
我们自行加锁后就成功让线程挨个操作data
'''
"""
GIL是一个纯理论知识 在实际工作中根本无需考虑它的存在
GIL作用面很窄 仅限于解释器级别
后期我们要想保证数据的安全应该自定义互斥锁(使用别人封装好的工具)
"""
验证多线程作用
"""
两个大前提
CPU的个数
单个
多个
任务的类型
IO密集型:经常拿走CPU使用
计算密集型:一直占着CPU
"""
# 单个CPU
多个IO密集型任务
多进程:浪费资源 无法利用多个CPU
多线程:节省资源 切换+保存状态
多个计算密集型任务
多进程:耗时更长 创建进程的消耗+切换消耗
多线程:耗时较短 切换消耗
'''多线程优势非常明显'''
# 多个CPU
多个IO密集型任务
多进程:浪费资源 多个CPU无用武之地
多线程:节省资源 切换+保存状态
多个计算密集型任务
多进程:利用多核 速度更快
多线程:速度较慢
'''多个IO密集型任务多线程更有优势
多个计算密集型任务多进程更有优势'''
结论:多进程和多线程都有具体的应用场景 尤其是多线程并不是没有用!!!
'''代码验证'''
import os
from multiprocessing import Process
from threading import Thread
import time
def work():
res = 1
for i in range(1,10000):
res *= i
# if __name__ == '__main__':
# print(os.cpu_count()) # 八核
# list = []
# start_time = time.time()
# for i in range(8):
# p = Process(target=work)
# p.start()
# list.append(p)
#
# for p in list:
# p.join()
# end_time = time.time()
# print(f'耗时:{start_time-end_time}') #耗时:-0.27302050590515137
list_t = []
start_time = time.time()
for i in range(8):
t = Thread(target=work)
t.start()
list_t.append(t)
for t in list_t:
t.join()
end_time = time.time()
print(f'耗时:{start_time-end_time}') # 耗时:-0.35170841217041016
"""
计算密集型
多进程
0.27302050590515137(一万次运算)
27.649538278579712(十万次运算)
多线程
0.35170841217041016(一万次运算)
76.02150464057922(十万次运算)
我用了一万次计算模拟,结果差了将近一秒
当模拟十万次运算的时候,结果差了将近两倍
结论
多进程更好,且处理的运算越多,优势越明显
"""
def work():
time.sleep(2) # 模拟纯IO操作
if __name__ == '__main__':
start_time = time.time()
# t_list = []
# for i in range(100):
# t = Thread(target=work)
# t.start()
# for t in t_list:
# t.join()
p_list = []
for i in range(100):
p = Process(target=work)
p.start()
for p in p_list:
p.join()
print('总耗时:%s' % (time.time() - start_time))
"""
IO密集型
多线程
总耗时:1.0338587760925293
多进程
总耗时:2.7993712425231934
两者翻了
结论
多线程更好
"""
![]()
死锁现象
# 锁就算掌握了如何抢 如何放 也会产生死锁现象
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()
print(f'{self.name}抢到了A锁')
mutexB.acquire()
print(f'{self.name}抢到了B锁')
mutexB.release()
mutexA.release()
def f2(self):
mutexB.acquire()
print(f'{self.name}抢到了B锁')
time.sleep(2)
mutexA.acquire()
print(f'{self.name}抢到了A锁')
mutexA.release()
mutexB.release()
for i in range(20):
t = MyThread()
t.start()
"""锁不能轻易使用并且以后我们也不会在自己去处理锁都是用别人封装的工具"""
信号量(了解)
信号量在不同的知识体系中 展示出来的功能是不一样的
eg:
在并发编程中信号量意思是多把互斥锁
在django框架中信号量意思是达到某个条件自动触发特定功能
"""
如果将自定义互斥锁比喻成是单个厕所(一个坑位)
那么信号量相当于是公共厕所(多个坑位)
"""
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()
# 只要是跟锁相关的几乎都不会让我们自己去写 后期还是用模块
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)
t = Thread(target=light)
t.start()
for i in range(20):
t = Thread(target=car, args=('熊猫PRO%s' % i,))
t.start()
# 这种效果其实也可以通过其他手段实现 比如队列(只不过没有event简便)
![]()
进程池与线程池(重点)
"""
补充:
服务端必备的三要素
1.24小时不间断提供服务
2.固定的ip和port
3.支持高并发
回顾:
TCP服务端实现并发
多进程:来一个客户端就开一个进程(临时工)
多线程:来一个客户端就开一个线程(临时工)
问题:
计算机硬件是有物理极限的 我们不可能无限制的创建进程和线程
措施:
池:
保证计算机硬件安全的情况下提升程序的运行效率
进程池:
提前创建好固定数量的进程 后续反复使用这些进程(合同工)
线程池:
提前创建好固定数量的线程 后续反复使用这些线程(合同工)
如果任务超出了池子里面的最大进程或线程数 则原地等待
强调:
进程池和线程池其实降低了程序的运行效率 但是保证了硬件的安全!!!
"""
# 代码演示(掌握)
import time
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
from threading import current_thread
pool = ThreadPoolExecutor(5) # 创建线程池,括号内是最大线程数,默认cpu的五倍
poo1 = ProcessPoolExecutor(5) # 创建进程池,最大允许5个进程,默认就是cpu的个数
def work(i):
time.sleep(1)
print(i)
# print(current_thread().name) # 所有的线程最大编号为4,印证了线程池最大只有5个线程
return f'第{i}次执行完成'
def func(*args,**kwargs):
print(args[0].result())
# 线程池操作
for i in range(20):
# pool.submit(work,i) # 往线程池中提交任务(异步)
pool.submit(work,i).add_done_callback(func) # add_done_callback(func())当线程执行完有结果返回,自动给到括号内的方法并调用
# 进程池操作
for i in range(20):
pool.submit(work,i).add_done_callback(func)
协程
"""
进程:资源单位
线程:执行单位
协程:单线程下实现并发
并发的概念:切换+保存状态
首先需要强调的是协程完全是程序员自己意淫出来的名词!!!
对于操作系统而言之认识进程和线程
协程就是自己通过代码来检测程序的IO操作并自己处理 让CPU感觉不到IO的存在从而最大幅度的占用CPU
类似于一个人同时干接待和服务客人的活 在接待与服务之间来回切换!!!
"""
# 基本使用
# 保存的功能 我们其实接触过 yield 但是无法做到检测IO切换
import time
from gevent import spawn
from gevent import monkey;monkey.patch_all() # 固定编写 用于检测所有IO操作
def func1():
print('func1开始执行了')
time.sleep(5)
print('func1执行结束了')
def func2():
print('func2开始执行了')
time.sleep(3)
print('func2结束执行')
start_time = time.time()
# # 直接挨个执行 # 耗时:8.03865933418274
# func1()
# func2()
# 协程执行 # 耗时:5.0301878452301025
g1 = spawn(func1) # 创建协程,跟连接池语法差不多
g2 = spawn(func2)
g1.join()
g2.join()
end_time = time.time()
print(f'耗时:{end_time-start_time}')
输出结果:
func1开始执行了
func2开始执行了
func2结束执行
func1执行结束了
耗时:5.0301878452301025
'''
结果可知,func1进行IO时,直接开始执行func2,由于func2IO时间短,所以func2先执行完
'''
基于协程实现TCP服务端并发
import socket
from gevent import monkey;monkey.patch_all()
from gevent import spawn
# 通信方法
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)
g = spawn(get_server)
g.join()
"""
终极结论
python可以通过开设多进程 在多进程下开设多线程 在多线程使用协程
从而让程序执行的效率达到极致!!!
但是实际业务中很少需要如此之高的效率(一直占着CPU不放)
因为大部分程序都是IO密集型的
所以协程我们知道它的存在即可 几乎不会真正去自己编写
"""
![]()