python子线程中执行事件循环

问题: sanic中使用用redis的订阅发布实现功能,如果没有发布的情况下,redis是阻塞在订阅处的,导致sanic无法处理路由。
原因: sanic自带的websokcet是协程执行的(在一个线程中循环执行一个事件循环),redis也是单线程的,订阅时如果没有任何消息发布,线程会阻塞在订阅消息处,导致线程阻塞,由于整个系统只在一个线程中运行,所以sanic无法处理其他路由

from sanic import Sanic,response
import time
import asyncio
import threading
import redis
import json

app=Sanic()
@app.websocket('/websocket')
async def websockethello(request,ws):
    await ws.send('hello')
    r=redis.Redis()
    p=r.pubsub()
    p.subscribe('test')
    p.parse_response()
    while True:
        print(p.parse_response())#----线程阻塞位置----
        await asyncio.sleep(5)
@app.route('/print')
async def httphello(request):
    return response.text('Hello')
app.run(host="0.0.0.0",port=9000,workers=1)

如前端没有连接websocket,路由print可正常执行,一旦前端连接websokcet之后,线程会执行while True,运行到print(p.parse_response())处会阻塞等待redis发布消息
解决:

  • 使用异步的reids模块aioredis
  • 开启一个子线程专门用来处理redis订阅并发送到websocket
from sanic import Sanic,response
import time
import asyncio
import threading
import redis
import json

app=Sanic()

class webSockets:
    __webSocketsList={}
    __webSocketNum=0
    
    def add(self,ws,request):
        self.__webSocketNum=self.__webSocketNum+1
        print(ws.remote_address)
        self.__webSocketsList[ws]=request
    
    def remove(self,ws):
        self.__webSocketNum=self.__webSocketNum-1
        self.__webSocketsList.pop(ws)
    
    async def print(self,message):
        print(len(self.__webSocketsList))
        for ws,request in self.__webSocketsList.items():
            print(ws.remote_address)
            await ws.send(message)

webSockets=webSockets()

@app.websocket('/websocket')
async def websockethello(request,ws):
    await ws.send('hello')
    webSockets.add(ws,request)
    while str(ws.state) == 'State.OPEN':
        await asyncio.sleep(20)
    webSockets.remove(ws)

@app.route('/print')
async def httphello(request):
    await webSockets.print('调用print接口')
    return response.text('Hello')


def thread_loop_task(loop):
    asyncio.set_event_loop(loop)

    async def sub():
        r=redis.Redis()
        p=r.pubsub()
        p.subscribe('test')
        p.parse_response()
        while True:
            message=p.parse_response()
            topic,content=message[1].decode('utf-8'),message[2].decode('utf-8')
            print('发布主题:',topic,'发布内容:',content)
            await webSockets.print(json.dumps({topic:content}))
    
    future=asyncio.gather(sub())
    loop.run_until_complete(future)

if __name__=="__main__":
    thread_loop=asyncio.new_event_loop()
    t=threading.Thread(target=thread_loop_task,args=(thread_loop,))
    #t=threading.Thread(target=sub)
    t.setDaemon(True)
    t.start()
    app.run(host="0.0.0.0",port=9000,workers=1)
    
  1. 首先创建一个全局的websocket列表,有websokcet连接时就加入到列表中,检测到此连接断开时就从列表中删除(此处是个人的一个需求),具体的代码在class webSockets:处,用于维护全局websocket列表的一个类,@app.websocket('/websocket')处用于处理websocket的状态,有新连接时就加入到列表中webSockets.add(ws,request)add函数的形参可改变,每隔20s检测一下websocket的状态,如果已经关闭就从列表中删除:
while str(ws.state) == 'State.OPEN':
        await asyncio.sleep(20)
        webSockets.remove(ws)

此处的时间可更改,实际操作中发现20s的时间有点长

  1. 协程websocket发送
async def print(self,message):
        print(len(self.__webSocketsList))
        for ws,request in self.__webSocketsList.items():
            print(ws.remote_address)
            await ws.send(message)

此函数是向前端发送消息,ws.send(message)是sanic自带的发送函数,当调用此print接口时,会向websocket列表中所有的连接发送消息,此处也可根据传入的参数不同向指定用户发送(此功能还未实现),此接口是协程函数,无法直接执行,需注册到时间循环中方可执行

  1. 打印路由接口
@app.route('/print')
async def httphello(request):
    await webSockets.print('调用print接口')
    return response.text('Hello')

由于sanic采用asyncio框架,async路由会自动(sanic底层库实现,对于用户来说是自动的)注册到时间循环中,所以在调用webSockets.print()接口时会执行

  1. 增加redis订阅发布功能,重新开启一个线程专门用来处理redis订阅,在子线程订阅时同样会阻塞,但是不影响主线程执行,路由接口在主线程中执行,此时就可实现主线程处理路由接口,子线程阻塞订阅,当有消息发布时发送到websocket连接中
def thread_loop_task(loop):
    asyncio.set_event_loop(loop)

    async def sub():
        r=redis.Redis()
        p=r.pubsub()
        p.subscribe('test')
        p.parse_response()
        while True:
            message=p.parse_response()
            topic,content=message[1].decode('utf-8'),message[2].decode('utf-8')
            print('发布主题:',topic,'发布内容:',content)
            await webSockets.print(json.dumps({topic:content}))
    
    future=asyncio.gather(sub())
    loop.run_until_complete(future)

主函数中:

thread_loop=asyncio.new_event_loop()
t=threading.Thread(target=thread_loop_task,args=(thread_loop,))
#t=threading.Thread(target=sub)
t.setDaemon(True)
t.start()

完整的后端代码参考上边
结果:
python子线程中执行事件循环_第1张图片

你可能感兴趣的:(python)