【python单线程】flask项目在服务器启动方法尝试 & GIL锁的释放 & gunicorn和gevent.pyWSGIServer的比较
1、GIL锁问题(在同一时刻,只有一个线程可以获取CPython解释器)
GIL
锁的介绍参考Python-- GIL 锁简述、python中的GIL详解
1)GIL
锁概述
核心点如下:
GIL是CPython特有,而Python还包括JPython,Psyco等
GIL 锁是加在 CPython 解释器上的,在 CPython 解释器中,GIL 是一把互斥锁,用来阻止同一个进程下多个线程的同时执行
GIL 锁是加在 CPython 解释器上的,进程先获取 GIL 锁,再获取 CPython 解释器。
为什么要使用GIL锁:
Python 是一门解释型的语言,这就意味着代码是解释一行,运行一行,它并不清楚代码全局;
因此,每个线程在调用 cpython 解释器 在运行之前,需要先抢到 GIL 锁,然后才能运行。
编译型的语言就不会存在 GIL 锁,编译型的语言会直接编译所有代码,就不会出现这种问题。
对象GIL锁举例:
启动 10 个线程 去修改同一个变量–number
方案一:在线程运行的函数中加入
time.sleep(0.1)
,而且没有加入数据的互斥锁– 结果为 9 而不是 0,运行时间为 0.1 秒左右import time from threading import Thread,Lock # mutex = Lock() number = 10 def func(): global number tem = number time.sleep(0.1) number = tem -1 if __name__ == '__main__': thread_list = [] for i in range(10): thread = Thread(target=func) thread.start() thread_list.append(thread) for i in thread_list: i.join() print(number)
加上
time.sleep(0.1)
,会让所有的线程将获得的 GIL 锁运行到这一行代码直接释放掉,其他的线程就能获取到 GIL 锁运行的步骤为:10 个线程会竞争 GIL 锁,然后运行代码,运行到 time.sleep(0.1),释放 GIL 锁,在等待期间,其他线程获取到 GIL 锁运行代码,再释放GIL 锁… 直到第10 个线程释放 GIL 锁之后,此时10 个进程都已经获取到了 tem = 10。
然后,第一个线程再获取 GIL 锁运行代码,修改 number = 10 - 1 = 9 , 修改结束,第一个线程运行结束,释放 GIL 锁;
其次,第二个线程再获取 GIL 锁运行代码,修改 number = 10 - 1 = 9 , 修改结束,第一个线程运行结束,释放 GIL 锁;
直到全部线程结束,那么结果就是 9 而不是 0
方案二:在线程运行的函数中 删除 time.sleep(0.1),而且没有加入数据的互斥锁– 结果为 0,运行时间为 0.1 秒左右
import time from threading import Thread,Lock # mutex = Lock() number = 10 def func(): global number tem = number # time.sleep(0.1) number = tem -1 if __name__ == '__main__': thread_list = [] for i in range(10): thread = Thread(target=func) thread.start() thread_list.append(thread) for i in thread_list: i.join() print(number)
因为
time.sleep(0.1)
所代表的的操作是 IO 操作,当线程运行到这一行代码的时候,要进行 IO 操作,需要释放 CPU 资源,也就是说,线程这个时候需要等待 IO操作结束,此时线程就会处于"阻塞态" 可以简单理解为这个线程什么活都不干了,就等着 IO结束,那么这个线程不干活了,其他线程要干活啊,所以,其他的线程就会获取 GIL 锁,再运行代码。如果没有
time.sleep(0.1)
所代表的的操作是 IO 操作,线程就会一直运行到结束,才释放 GIL 锁,其他的线程才能获取 GIL 锁,运行代码;那么,反映在这个例子中,第一个线程获取 GIL 锁以后,会直接运行到最后一步,修改了
number = tem -1=10-1=9
;下一个线程,再获取到的
number
就是 9 了,依次类推,结果为 0;
2)项目中因为GIL
锁存在的问题
现在项目中存在这个场景:
在进行人脸身份识别的界面上,用户可以选择导入视频,也可以选择打开摄像头进行身份的识别。如果视频还没有解析处理完,用户就跳到了其他界面上,这时需要把用户创建的
camera
对象进行销毁,避免其一直调用camera.read()
读取下一个视频帧。进行人脸身份识别后,逐帧返回二进制图片流的视图函数:
'''predict_fire POST请求返回的路由,用于给前端发送二进制视频流''' @faceRecogB.route('/get_faceRecog_frame', methods=['GET']) def get_faceRecog_frame(): username = "wang" file_path = request.args.get("file_path") type = int(request.args.get("type")) frame = frameDetect_handler.handle_frame_with_type(username, request, type, faceRecog_Detector, file_path,frameDraw=3) # 完成图片、视频上传到本地 return Response(frame, mimetype='multipart/x-mixed-replace; boundary=frame') # return jsonify({'finish' : True}) #已经检测完毕
人脸识别摄像头关闭函数:
'''关闭指定用户的摄像头对象''' @faceRecogB.route('/shutdown_faceRecog_analysis', methods=['GET']) def close_webCamera(): username = "wang" if (request.method == 'GET'): frameDetect_handler.clear_camera(username) #先释放摄像头 totalPaths = glob.glob(os.path.join(temp_save_path,"*.*")) for path in totalPaths: os.remove(path) # 这个可以删除单个文件,不能删除文件夹 return jsonify({'code': 200, 'msg': '操作成功', 'data' : True}) else: return jsonify({'code': 400, 'msg': '操作失败:请使用get方法'})
其中这两个函数调用的
camera
这一共享变量。
在使用app.run
时,偶尔会出现camera
对象无法释放,视频继续分析/本地摄像头无法关闭的这个问题:
app.run(debug=True,threaded=True,host=config['ip'],port=config['port'],ssl_context=('client-1.local.crt','client-1.local.key')) #development env
猜测app.run()
开启的多线程虽然是单个进程下的多线程,但是采用了时间片轮询机制,当线程结束时会释放GIL
锁。
在执行gunicorn
时,这个问题就会一直存在:
gunicorn -w 5 -b 0.0.0.0:5000 --certfile=client-1.local.crt --keyfile=client-1.local.key manage:app
gunicorn
设置的worker
数即为进程数,即使后面在处理一个请求时调用time.sleep
主动释放GIL锁,在执行如下命令时
gunicorn -w 5 -b 0.0.0.0:5000 -k 'gevent' --certfile=client-1.local.crt --keyfile=client-1.local.key manage:app
close_camera
仍然无法抢占并释放camera
这个共享变量,需要等到 get_faceRecog_frame
执行完毕之后才释放GIL
锁。
3)可以避开GIL
全局锁的方法
参考
GIL
锁机制限制了python
的多线程,那有什么办法在python
上实现高并发呢?
- 将多线程方法改为多进程(比如
gunicorn
) - 引入协程(比如
gevent
)
Note:多进程与多线程的区别:
-
进程有自己的独立地址空间, 建立数据表来维护代码段, 堆栈段和数据段, 而线程共享进程中的资源, 使用相同的地址空间, 所以线程间的切换快得多.
-
因为线程共享进程的全局变量, 静态变量等对象, 线程间的通信更为方便, 而进程间的通信更加复杂, 需要以ipc的方式进行.
-
多进程要比多线程要健壮,进程之间一般不会相互影响, 而多线程有一条线程崩溃, 会导致整个进程跟着发生崩溃或者无法正常退出等.
2、尝试多种服务器启动方式(gunicorn
开启多个进程 & gevent.pyWSGIServer
多个协程绑定一个线程)
1)在服务器上使用gunicorn
出现请求超时(不符合项目场景)
Note:
该方法存在两个问题:
由于
CPython
的GIL锁机制,某一时刻只允许一个线程获取CPython解释器;
gunicorn
存在请求超时自动重启服务的问题;
由于gunicorn
的机制是:请求超时,重启服务(并非是服务器定时向gunicorn发送心跳,进行心跳检测)。导致二进制视频数据流无法长时间的传输。
由于在进行人脸视频检测,舱门视频检测时,容易出现长时间返回二进制数据流的时候,即没有返回响应状态码,比如:
'''predict_fire POST请求返回的路由,用于给前端发送二进制视频流''' @faceRecogB.route('/get_faceRecog_frame', methods=['GET']) def get_faceRecog_frame(): username = "wang" file_path = request.args.get("file_path") type = int(request.args.get("type")) frame = frameDetect_handler.handle_frame_with_type(username, request, type, faceRecog_Detector, file_path,frameDraw=3) # 完成图片、视频上传到本地 return Response(frame, mimetype='multipart/x-mixed-replace; boundary=frame') # return jsonify({'finish' : True}) #已经检测完毕
TIMEOUT
默认为30s
,如果worker不能在30s
进行返回结果,gunicorn
就认定这个request超时,并重启服务如果是如下这种有状态码返回的形式,就不存在超时行为
'''关闭指定用户的摄像头对象''' @faceRecogB.route('/shutdown_faceRecog_analysis', methods=['GET']) def close_webCamera(): username = "wang" if (request.method == 'GET'): frameDetect_handler.clear_camera(username) #先释放摄像头 totalPaths = glob.glob(os.path.join(temp_save_path,"*.*")) for path in totalPaths: os.remove(path) # 这个可以删除单个文件,不能删除文件夹 return jsonify({'code': 200, 'msg': '操作成功', 'data' : True}) else: return jsonify({'code': 400, 'msg': '操作失败:请使用get方法'})
所以为了让请求具有长连接,可以如下命令作为弥补方案,参考gunicorn timeout, Gunicorn服务报错-WORKER TIMEOUT
gunicorn -w 15 -t 250 -b 0.0.0.0:5000 --certfile=client-1.local.crt --> keyfile=client-1.local.key manage:app
其中使用 15个 worker 进程(
-w 15
)来运行 Flask 应用,绑定到localhost
的5000
端口(-b 127.0.0.1:5000
)#运行效果 [2022-11-07 11:48:54 +0000] [31360] [INFO] Starting gunicorn 20.1.0 [2022-11-07 11:48:54 +0000] [31360] [INFO] Listening at: https://0.0.0.0:5000 (31360) [2022-11-07 11:48:54 +0000] [31360] [INFO] Using worker: gevent [2022-11-07 11:48:54 +0000] [31363] [INFO] Booting worker with pid: 31363 [2022-11-07 11:48:54 +0000] [31364] [INFO] Booting worker with pid: 31364 [2022-11-07 11:48:54 +0000] [31365] [INFO] Booting worker with pid: 31365 [2022-11-07 11:48:54 +0000] [31366] [INFO] Booting worker with pid: 31366 [2022-11-07 11:48:54 +0000] [31367] [INFO] Booting worker with pid: 31367 [2022-11-07 11:48:54 +0000] [31368] [INFO] Booting worker with pid: 31368 [2022-11-07 11:48:54 +0000] [31369] [INFO] Booting worker with pid: 31369 [2022-11-07 11:48:54 +0000] [31370] [INFO] Booting worker with pid: 31370
如果不想要根据
TIMEOUT
来重启服务的话,建议使用gevent
,参考How disable gunicorn timeout的回答。
2)使用gevent.pywsgi
运行flask并配置ssl(不符合场景)
Note:
存在问题:不像
gunicorn
会开启多个线程来处理request请求,gevent
的WSGIServer
在启动服务时只会通过单线程来处理request请求
- 有点类似于
javascript
中的迭代器实现异步的过程,而且js
也是个单线程语言;参考JavaScript中的协程- 有点类似java中
Executors.newSingleThreadExecutor();
单个线程的线程池,不同的是gevent
在用户态引入了协程的概念,其WSGIServer
实现了多个协程与单个线程的绑定)。
参考
协程有分为原生协程和第三方库(gevent模块)实现的协程两种。
gevent
其原理是当一个协程遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
单线程启动项目(但是这样存在问题:单线程,如果某一个线程阻塞,其他请求均不会得到处理):
# app.py
from gevent import pywsgi
from app import create_app
from flask import flask
import gevent.monkey
gevent.monkey.patch_all() #request仍然堵塞,无效果
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
if __name__ == '__main__':
app.debug = True #无效
app.threaded= True #无效
#WSGI: Web Server Gateway Interface。
server = pywsgi.WSGIServer( ('127.0.0.1', 5000), app, keyfile='client-1.local.key', certfile='client-1.local.crt')
server.serve_forever()
如何通过gevent
开启flask
的多线程?Gevent pywsgi WSGIServer concurrent request are queued,作者的回答是:
gevent’s WSGIServer uses greenlets for concurrency. If your code is cooperative with gevent and greenlets, than it certainly can handle requests concurrently.
意思是说:
gevent
是一个并发模块(可以实现协程),在使用gevent.pywsgi
时,要想让服务器实现并发操作,必须在代码中使用gevent
和greenlets
来处理并发问题。否则gevent.monkey.patch_all()
没有效果。
3)使用gunicorn + gevent + flask
实现高并发(不符合场景)
Note:
存在问题:
运行超时重启服务。
不同线程/进程访问同一个对象存在
GIL
问题
如何在flask
中使用gevent
协程,实现并发处理 ?可以采用gunicorn
运行WSGIServer
,参考flask+Gunicorn(gevent)高并发的解决方法探究_adamyoungjack的博客-CSDN博客_flask高并发解决方案
gunicorn -w 5 -b 0.0.0.0:5000 -k 'gevent' --certfile=client-1.local.crt --keyfile=client-1.local.key manage:app
4)最终解决方法(gevent + sleep)
在循环处理视频时,增加time.sleep(0.001)
,让线程主动释放GIL
锁
'''检测上传的视频'''
def gen_frames(camera,detector,frameDraw=1,alive_beat=20):
'''
:param camera: 摄像头实例对象
:param detector: 检测器
:param frame_path: 视频帧路径
:param alive_beat: 心跳检测,如果10帧都未能正常读取,则认为camera已读取完毕
:return: yield frame
'''
frame_count = 0
alive_beat_count = 0
while camera.isOpened():
print("hello_handle_video")
frame_count += 1
# 一帧帧循环读取摄像头的数据
#将读取视频帧的任务交给videoStream类来处理(决定是返回单帧frame,还是count帧frames)
success, frame = camera.read()
if success and (frame_count % frameDraw == 0): #抽帧处理
alive_beat_count = 0
frame = cv2.resize(frame, (640, 480))
frame = detector.detect(frame)
ret, buffer = cv2.imencode('.jpg', frame)
frame = buffer.tobytes()
# 使用yield语句,将帧数据作为响应体返回,content-type为image/jpeg
time.sleep(0.001) #释放CPython解释器的GIL锁,让其他线程可以关闭camera
yield (b'--frame\r\n' +
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
alive_beat_count += 1
if(alive_beat_count > alive_beat):
print("video_is_off")
break
camera.release()
pyWSGIServer
代码如下
from gevent import monkey
monkey.patch_all() # 对所有的IO和耗时操作打补丁
import os
import sys
from app import create_app
import argparse
from app.blueprints.view_utils.yaml_load import load_yaml,write_yaml
curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = curPath
sys.path.append(rootPath)
# 启动脚本
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
#更新server_config.yml
def update_server_config(config):
# 将ip,port写入server_config.yml,减少手动配置
source_path = "server_config.yml"
# source_path = "server_config_withDB.yml" #使用数据库
# 先读取,再写入
server_config = load_yaml(source_path)
server_config['ip'] = config['ip']
server_config['port'] = config['port']
#判断id_dataset_save_path路径是否存在,如果不存在则使用默认路径
if(not os.path.isdir(server_config["id_dataset_save_path"])):
server_config["id_dataset_save_path"] = os.path.join(rootPath,"temp/id_dataset")
#判断buffer_path.prefix_dir 路径是否存在,如果不存在则使用默认路径
if (not os.path.isdir(server_config["buffer_path"]['prefix_dir'])):
server_config["buffer_path"]["prefix_dir"] = os.path.join(rootPath, "temp/buffers")
write_yaml(server_config, source_path)
#手动配置 server_config.yml
if __name__ == '__main__':
config = {
'ip': "127.0.0.1", # 局域网ip
'port': 5000,
}
# ssl = {
# 'certfile': 'client-1.local.crt',
# 'keyfile': 'client-1.local.key',
# }
parser = argparse.ArgumentParser(
description="show example") # 这一行是argparse的构造函数,当其中description参数指定了当用户敲入-h时显示的模块介绍。
parser.add_argument("-i", "--ip", type=str, default=config['ip'], help="ip地址") # ip地址。
parser.add_argument("-p", "--port", type=int, default=5000, help="端口号") # 端口号。
ARGS = parser.parse_args()
if ARGS.ip:
config['ip'] = ARGS.ip
if ARGS.port:
config['port'] = ARGS.port
update_server_config(config)
from gevent.pywsgi import WSGIServer
http_server = WSGIServer((config['ip'], config['port']), app.wsgi_app, spawn=100, keyfile='client-1.local.key', certfile='client-1.local.crt')
http_server.serve_forever()
项目启动命令为
python manage_gevent.py --ip="0.0.0.0" -p 5000