验证GIL的存在与特点、验证python多线程性能、死锁现象以及进程池线程池等
验证GIL的存在
from threading import Thread
money =100
def task():
global money
money -=1
t_list = [] # 存储每个线程对象
for i in range(100):
t= Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join() # 100个线程都开启之后在使用join方法,确保所有线程结束在打印money
print(money)
验证GIL的特点
from threading import Thread
import time
money =100
def task():
global money
temp = money
time.sleep(1) # IO会导致所有的线程在依次抢到GIL锁后也要被迫释放
money=temp-1
t_list = []
for i in range(100):
t= Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join() # 等待所有线程运行结束在打印money
print(money) # 99
from threading import Thread,Lock
import time
money=100
mutex=Lock()
def task():
mutex.acquire() # 抢锁
global money
temp=money
time.sleep(0.02)
money=temp-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()
print(money) # 0
验证python多线程是否有用
需要分以下几种情况
情况一:
单个CPU
多个CPU
情况二:
IO密集型(代码有IO操作)
计算密集型(代码没有IO)
- 单个CPU
IO密集型
多进程:申请额外的空间,消耗更多的资源
多线程:消耗资源相对较少,通过多道技术
计算密集型
多进程:申请额外的空间,消耗更多的资源(总耗时+申请空间+拷贝代码+切换)
多线程:消耗资源相对较少,通过多道技术(总耗时+切换)
由上可以看出在单U情况下无论是IO密集型还是计算密集型,多线程都更加有优势
- 多个CPU
IO密集型:
多进程:总耗时(单个进程的耗时+申请空间+拷贝代码)
多线程:总耗时(单个进程的耗时+IO)
计算密集型
多进程:总耗时(单个进程的耗时)
多线程:总耗时(多个进程的综合耗时)
当IO密集型时,多线程有优势,当计算密集型时,多进程更有优势
代码演示
import os
from multiprocessing import Process
from threading import Thread
import time
# 计算密集型
def work():
res=1
for i in range(1,100000):
res *=i
if __name__ == '__main__':
# print(os.cpu_count()) # 8 ,所以创建8个进程
start_time = time.time()
p_list = []
# t_list = []
for i in range(8):
p = Process(target=work)
p.start()
p_list.append(p)
for p in p_list:
p.join()
# t= Thread(target=work)
# t.start()
# t_list.append(t)
# for t in t_list:
# t.join()
print('总耗时: %s'% (time.time()-start_time))
"""
计算密集型
多进程: 总耗时>>> 5.5769689083099365
多线程:总耗时>>> 22.86092185974121
"""
def work():
time.sleep(2)
if __name__ == '__main__':
start_time= time.time()
# t_list = []
p_list = []
for i in range(100):
# t = Thread(target=work)
# t.start()
# t_list.append(t)
# for t in t_list:
# t.join()
p = Process(target=work)
p.start()
p_list.append(p)
for p in p_list:
p.join()
print('总耗时>>> %s' % (time.time()-start_time))
"""
计算密集型
总耗时>>> 2.010854721069336
总耗时>>> 3.6308200359344482
"""
死锁现象
定义:指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁
虽然我们已经掌握了互斥锁的使用(先抢锁,后释放锁),但是在实际项目中也应该尽量少用,因为用的不好,会产生死锁现象(如下代码所示)
from threading import Thread,Lock
import time
mutexA = Lock() # 锁A
mutexB = Lock() # 锁B
class MyThread(Thread):
def run(self):
self.fun1()
self.fun2()
def fun1(self):
mutexA.acquire()
print(f'{self.name}抢到了A锁')
mutexB.acquire()
print(f'{self.name}抢到了B锁')
mutexB.release()
print(f'{self.name}释放了B锁')
mutexA.release()
print(f'{self.name}释放了A锁')
def fun2(self):
mutexB.acquire()
print(f'{self.name}抢到了B锁')
time.sleep(1)
mutexA.acquire()
print(f'{self.name}抢到了A锁')
mutexA.release()
print(f'{self.name}释放了A锁')
mutexB.release()
print(f'{self.name}释放了B锁')
for i in range(10):
t = MyThread()
t.start()
"如下图所示,发生了死锁现象"
信号量
信号量本质也是互斥锁,只不过它是多把锁
信号量在不同的知识体系中,意思也可能有区别
- 在并发编程中,信号量就是多把互斥锁
- 在django中,信号量指的是达到某个条件自动触发(中间件)
我们之前使用Lock产生的是单把锁,信号量是多把锁
from threading import Thread,Semaphore
import time
import random
sp = Semaphore(5) # 一次性产生5把互斥锁
class MyThread(Thread):
def run(self):
sp.acquire()
print(f'{self.name}抢到了锁')
time.sleep(random.random())
sp.release()
print(f'{self.name}释放了锁')
for i in range(20):
t = MyThread()
t.start()
event事件
event事件是指子进程或子线程之间可以彼此等待彼此
例如:子线程A 运行到某一个代码位置发送信告诉子线程B开始运行
import time
from threading import Thread,Event
event=Event()
def light():
print('红灯亮着的,所有车辆不准通行')
time.sleep(2)
event.set()
def car(name):
print(f'{name}正在等红灯')
event.wait()
print(f'{name}加油门,飙车了')
t= Thread(target=light)
t.start()
for i in range(20):
t = Thread(target=car,args=(f'第{i}辆车',))
t.start()
进程池和线程池
受限于硬件水平的原因,所以我们在实际应用中是不可以无限制的开进程和线程,
我们开设多进程和多线程的时候,还要考虑硬件的承受范围,在实际开发过程中,我们可以通过使用池来保证计算机硬件和程序的稳定。
-
池:降低程序的执行效率,保证计算机硬件的安全
-
进程池:提前创建好固定个数的进程供程序使用,后续不会再创建
-
线程池:提前创建好固定个数的线程供程序使用,后续不会再创建
from concurrent.futures import ThreadPoolExecutor
from threading import current_thread
import time
pool = ThreadPoolExecutor(5) # 产生一个固定只有5个线程的线程池
# 线程执行函数
def task():
time.sleep(1)
return f'{current_thread().name}结束了任务'
# 回调函数
def func(*args,**kwargs):
print(args) # (<Future at 0x7f7dad291e80 state=finished returned str>,)
print(args[0].result()) # 打印线程的回调结果
for i in range(100):
pool.submit(task).add_done_callback(func)
"""
pool.submit(task): 朝池子中提交任务(异步)
add_done_callback(func):异步回调,自动获取线程执行函数的返回值,并传到回调函数func内
"""
# 进程的代码也是如此
协程
进程是资源单位,线程是执行单位,而协程是在单线程下实现并发(效率极高)
协程的实现原理是在代码层面欺骗CPU,让CPU觉得我们的代码里面没有IO操作,在实际上IO操作被我们自己写的代码检测,一旦有,立刻让代码执行别的(该技术完全是程序员自己弄出来的,名字也是程序员自己起的)
协程的核心就是自己写代码完成切换+保存状态
from gevent import monkey;monkey.patch_all()
from gevent import spawn
import time
def func1():
print('func1 running')
time.sleep(2)
print('func1 over')
def func2():
print('func2 running')
time.sleep(4)
print('func2 over')
if __name__ == '__main__':
start_time = time.time()
s1 = spawn(func1) # 检测代码,一旦有IO自动切换程序(执行没有IO的操作,变向的等待IO结束)
s2 = spawn(func2)
s1.join()
s2.join()
print(time.time()-start_time)
协程实现TCP服务端并发
import socket
from gevent import monkey;monkey.patch_all() # 固定编写,用于检测代码中所有的IO操作(猴子补丁)
from gevent import spawn
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)
def communication(sock):
while True:
data = sock.recv(1024)
print(data.decode('utf8'))
sock.send(data.upper())
p=spawn(get_server)
p.join()