Bootstrap

fastapi 多线程非阻塞启动

uvicorn 启动 fastapi

参考: https://www.cnblogs.com/selfcs/p/17240902.html

阻塞启动

在使用 uvicorn 启动之后,程序进入阻塞状态。其他的程序、函数都不会执行。

from fastapi import FastAPI
import uvicorn

app = FastAPI(
    title="test",
    description="test",
    version="0.1",
)

@app.get("/")
async def read_root():
    return {"Hello": "World"}

@app.get("/stop")
async def ctrl_stop():
    return {"status": "stop"}

if __name__ == '__main__':
    uvicorn.run(app, host="0.0.0.0", port=9898)

运行结果:

PS D:\llm\> python.exe .\thread.py
INFO:     Started server process [88384]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:9898 (Press CTRL+C to quit)

非阻塞启动

import time

import threading
from fastapi import FastAPI
import contextlib
import uvicorn

# def on_start():
#     print("start----------")

# def on_stop():
#     print("stop-----------")

# app = FastAPI(on_startup=[on_start], on_shutdown=[on_stop])
# # @app.on_event("shutdown")
# # async def shutdown():
# #     print("##############################################################3 shutdown")

app = FastAPI(
    title="test",
    description="test",
    version="0.1",
)

@app.get("/")
async def read_root():
    return {"Hello": "World"}

@app.get("/stop")
async def ctrl_stop():
    return {"status": "stop"}

class UvicornServer(uvicorn.Server):
    def install_signal_handlers(self):
        pass

    # 上下文管理器 
    # 上下文管理器是指:在一段代码,执行之前执行一段代码,用于一些预处理工作;执行之后再执行一段代码,用于一些清理工作。
    @contextlib.contextmanager
    def run_in_thread(self):
        print("UvicornServer start thread ######### 1")
        thread = threading.Thread(target=self.run)
        thread.start()
        try:
            print("UvicornServer in start", self.started)
            while not self.started:
                time.sleep(1e-3)    # 1e-3 = 0.001
            yield
        finally:
            print("UvicornServer in exit", self.started)
            self.should_exit = True
            thread.join()
        print("UvicornServer start thread ######### 2")

if __name__ == '__main__':
    # uvicorn.run(app, host="0.0.0.0", port=9898)
    config = uvicorn.Config(app=app, host="0.0.0.0", port=9898, log_level="info")
    server = UvicornServer(config=config)

    with server.run_in_thread():
        print("你要执行的其他程序")
        count=0
        while (count < 10):
            print(f"sleep {count}")
            time.sleep(1)
            count+=1

运行结果:
启动thread.py 之后 调用:http://127.0.0.1:9898/stop

PS D:\llm\> python.exe .\thread.py
UvicornServer start thread ######### 1
UvicornServer in start False
INFO:     Started server process [91432]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:9898 (Press CTRL+C to quit)
你要执行的其他程序
sleep 0
sleep 1
sleep 2
sleep 3
sleep 4
sleep 5
sleep 6
sleep 7
INFO:     127.0.0.1:55426 - "GET /stop HTTP/1.1" 200 OK
sleep 8
sleep 9
UvicornServer in exit True
INFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [91432]
UvicornServer start thread ######### 2

实例

import threading
from fastapi import FastAPI
import contextlib
import uvicorn
import time

def on_start():
    print("start----------")

def on_stop():
    print("stop-----------")

# app = FastAPI(on_startup=[on_start], on_shutdown=[on_stop])
# # @app.on_event("shutdown")
# # async def shutdown():
# #     print("##############################################################3 shutdown")

app_run_status = True
app = FastAPI(
    title="test", description="test", version="0.1",
    on_startup=[on_start], on_shutdown=[on_stop]
)

@app.get("/")
async def read_root():
    return {"Hello": "World"}

@app.get("/stop")
async def ctrl_stop():
    global app_run_status
    app_run_status = False
    return {"status": "stop"}

class UvicornServer(uvicorn.Server):
    def install_signal_handlers(self):
        pass

    # 上下文管理器 
    # 上下文管理器是指:在一段代码,执行之前执行一段代码,用于一些预处理工作;执行之后再执行一段代码,用于一些清理工作。
    @contextlib.contextmanager
    def run_in_thread(self):
        print("UvicornServer start thread ######### 1")
        thread = threading.Thread(target=self.run)
        thread.start()
        try:
            while not self.started:
                time.sleep(1e-3)    # 1e-3 = 0.001
            yield
        finally:
            self.should_exit = True
            thread.join()
        print("UvicornServer start thread ######### 2")

def start_server():
    # uvicorn.run(app, host="0.0.0.0", port=9898)
    config = uvicorn.Config(app=app, host="0.0.0.0", port=9898, log_level="info")
    server = UvicornServer(config=config)

    with server.run_in_thread():
        while app_run_status:
            print("UvicornServer", app_run_status)
            time.sleep(1)

if __name__ == '__main__':
	# 启动 uvicorn 线程
    th = threading.Thread(target=start_server)
    th.start()

	# 主线程等待 20s自动停止
    count = 0
    while count < 20:
        print(f"main sleep {count}")
        time.sleep(1)
        count+=1

运行结果:
启动 thread.py 之后 调用:http://127.0.0.1:9898/stop 停止uvicorn线程

PS D:\llm\> python.exe .\thread.py
UvicornServer start thread ######### 1
main sleep 0
INFO:     Started server process [97212]
INFO:     Waiting for application startup.
start----------
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:9898 (Press CTRL+C to quit)
UvicornServer True
main sleep 1
UvicornServer True
main sleep 2
UvicornServer True
main sleep 3
UvicornServer True
main sleep 4
UvicornServer True
main sleep 5
UvicornServer True
INFO:     127.0.0.1:55609 - "GET /stop HTTP/1.1" 200 OK
main sleep 6
INFO:     Shutting down
INFO:     Waiting for application shutdown.
stop-----------
INFO:     Application shutdown complete.
INFO:     Finished server process [97212]
UvicornServer start thread ######### 2
main sleep 7
main sleep 8
main sleep 9
main sleep 10
main sleep 11
main sleep 12
main sleep 13
main sleep 14
main sleep 15
main sleep 16
main sleep 17
main sleep 18
main sleep 19

上下文管理器(Context Manager)

参考: https://zhuanlan.zhihu.com/p/24709718
上下文管理器是指:在一段代码,执行之前执行一段代码,用于一些预处理工作;执行之后再执行一段代码,用于一些清理工作。
比如打开文件进行读写,读写完之后需要将文件关闭。又比如在数据库操作中,操作之前需要连接数据库,操作之后需要关闭数据库。
在上下文管理协议中,有两个方法__enter__和__exit__,分别实现上述两个功能。

with 语法

讲到上下文管理器,就不得不说到 python 的 with 语法。基本语法格式为:

with EXPR as VAR:
    BLOCK

这里就是一个标准的上下文管理器的使用逻辑,稍微解释一下其中的运行逻辑:
(1)执行 EXPR 语句,获取上下文管理器(Context Manager)
(2)调用上下文管理器中的 enter 方法,该方法执行一些预处理工作。
(3)这里的 as VAR 可以省略,如果不省略,则将 enter 方法的返回值赋值给 VAR。
(4)执行代码块 BLOCK,这里的 VAR 可以当做普通变量使用。
(5)最后调用上下文管理器中的的 exit 方法。
(6)exit 方法有三个参数:exc_type, exc_val, exc_tb。如果代码块 BLOCK 发生异常并退出,那么分别对应异常的 type、value 和 traceback。否则三个参数全为 None。
(7)exit 方法的返回值可以为 True 或者 False。如果为 True,那么表示异常被忽视,相当于进行了 try-except 操作;如果为 False,则该异常会被重新 raise。

实例:自己实现打开文件操作

# 自定义打开文件操作
class MyOpen(object):

    def __init__(self, file_name):
        """初始化方法"""
        self.file_name = file_name
        self.file_handler = None
        return

    def __enter__(self):
        """enter方法,返回file_handler"""
        print("enter:", self.file_name)
        self.file_handler = open(self.file_name, "r")
        return self.file_handler

    def __exit__(self, exc_type, exc_val, exc_tb):
        """exit方法,关闭文件并返回True"""
        print("exit:", exc_type, exc_val, exc_tb)
        if self.file_handler:
            self.file_handler.close()
        return True

# 使用实例
with MyOpen("python_base.py") as file_in:
    for line in file_in:
        print(line)
        raise ZeroDivisionError
# 代码块中主动引发一个除零异常,但整个程序不会引发异常

内置库 contextlib 的使用

Python 提供内置的 contextlib 库,使得上线文管理器更加容易使用。其中包含如下功能:
(1)装饰器 contextmanager。该装饰器将一个函数中 yield 语句之前的代码当做 enter 方法执行,yield 语句之后的代码当做 exit 方法执行。同时 yield 返回值赋值给 as 后的变量。

@contextlib.contextmanager
def open_func(file_name):
    # __enter__方法
    print('open file:', file_name, 'in __enter__')
    file_handler = open(file_name, 'r')

    yield file_handler

    # __exit__方法
    print('close file:', file_name, 'in __exit__')
    file_handler.close()
    return

with open_func('python_base.py') as file_in:
    for line in file_in:
        print(line)

(2)closing 类。该类会自动调用传入对象的 close 方法。使用实例如下:

class MyOpen2(object):
    def __init__(self, file_name):
        """初始化方法"""
        self.file_handler = open(file_name, "r")
        return

    def close(self):
        """关闭文件,会被自动调用"""
        print("call close in MyOpen2")
        if self.file_handler:
            self.file_handler.close()
        return

with contextlib.closing(MyOpen2("python_base.py")) as file_in:
    pass

这里会自动调用 MyOpen2 的 close 方法。我们查看 contextlib.closing 方法,内部实现为:

class closing(object):
    """Context to automatically close something at the end of a block."""
    def __init__(self, thing):
        self.thing = thing
    def __enter__(self):
        return self.thing
    def __exit__(self, *exc_info):
        self.thing.close()

closing类的__exit__方法自动调用传入的thing的close方法。
(3)nested 类。该类在 Python2.7 之后就删除了。原本该类的作用是减少嵌套,但是Python2.7之后允许如下的写法:

with open("aaa", "r") as file_in, open("bbb", "w") as file_out:
    pass
;