【python单线程】flask项目在服务器启动方法尝试 & GIL锁的释放 & gunicorn和gevent.pyWSGIServer的比较

【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 timeoutGunicorn服务报错-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 应用,绑定到 localhost5000 端口( -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请求,geventWSGIServer在启动服务时只会通过单线程来处理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时,要想让服务器实现并发操作,必须在代码中使用geventgreenlets来处理并发问题。否则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
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,这是一个关于 Python Web 框架性能对比以及如何使用 GunicornGevent 提高 Python Web 框架性能的问题。下面是我的回答: Python Web 框架性能对比 Python 有很多优秀的 Web 框架,如 Django、Flask、Tornado、Bottle 等等。这些框架都有自己的特点和优缺点,但是在性能方面却有所不同。下面是一些常见 Python Web 框架的性能对比数据: 框架 | Requests/s -----|---------- Flask| 673.24 Django| 372.77 Bottle| 3,040.36 Tornado| 3,714.29 从上表可以看出,Tornado 和 Bottle 性能比较突出,而 Django 和 Flask 的性能稍逊一些。但是这些数据并不是绝对的,实际性能还需要根据具体的应用场景和实现方式进行测试和评估。 使用 GunicornGevent 提高 Python Web 框架的性能 GunicornGevent 都是 Python Web 服务器,它们可以与常见的 Python Web 框架配合使用,提高 Web 应用的性能。 具体来说,Gunicorn 是一个使用 Python 编写的 WSGI HTTP 服务器,可以用于部署 Django、Flask 等 Web 应用。Gunicorn 使用多进程的方式来提高并发处理能力,可以根据系统的 CPU 核数来设置进程数,同时还支持异步处理和负载均衡等特性。 Gevent 是一个基于协程的 Python 网络库,可以用于编写高性能的网络应用程序。Gevent 可以与 Python Web 框架配合使用,使用协程来处理请求,可以显著提高 Web 应用的并发处理能力和响应速度。 下面是一个使用 GunicornGevent 提高 Flask Web 应用性能的示例代码: ``` python from gevent import monkey monkey.patch_all() from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return 'Hello, World!' if __name__ == '__main__': from gunicorn.app.base import BaseApplication from gunicorn.six import iteritems class StandaloneApplication(BaseApplication): def __init__(self, app, options=None): self.options = options or {} self.application = app super(StandaloneApplication, self).__init__() def load_config(self): config = dict([(key, value) for key, value in iteritems(self.options) if key in self.cfg.settings and value is not None]) for key, value in iteritems(config): self.cfg.set(key.lower(), value) def load(self): return self.application options = { 'bind': '0.0.0.0:8000', 'workers': 4, } StandaloneApplication(app, options).run() ``` 上面的代码使用了 GunicornGevent启动一个 Flask Web 应用,同时设置了 4 个 worker 进程来处理请求。这样可以显著提高 Web 应用的性能和并发处理能力。 希望这个回答对你有帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值