如何在Plotly Dash应用中实现浏览器标签页后台时dcc.Interval稳定触发?
问题场景描述
我正在基于Plotly Dash开发一款Web应用原型,该应用需要定期通过串口外设执行数据采集操作。目前我使用dcc.Interval组件定时从传感器获取数据,并进行绘图与存储。但在Firefox或Chrome浏览器中,当应用所在标签页处于后台时,浏览器的性能节流机制会大幅降低dcc.Interval组件的触发频率,甚至会完全停止后台定时器的运行,相关问题已有相关文档记录。
最小可复现示例
以下示例仅在控制台输出计数,可复现后台标签页节流的问题:
import dash from dash.dependencies import Input, Output import dash_html_components as html import dash_core_components as dcc app = dash.Dash(__name__) app.layout = html.Div([ html.Div("A div", id="my_div"), dcc.Interval(id='my_interval', disabled=False, n_intervals=0), ]) @app.callback(Output('my_div', 'children'), [Input('my_interval', 'n_intervals')]) def update(n_intervals): print(n_intervals) return dash.no_update if __name__ == '__main__': app.run_server(debug=True, host='0.0.0.0', port=5000)
当标签页处于前台时,该示例正常运行,每秒递增计数(dcc.Interval组件的默认触发间隔为1秒),但切换到其他标签页后,触发间隔会逐渐变长。在这个简单示例中,该现象可能因浏览器和设备不同而存在差异,但在复杂应用中,后台状态下两次callback的间隔可达数秒甚至数十秒。
我的核心目标是让应用中的dcc.Interval组件始终保持相同的触发频率,希望能找到可行的解决方法,或让浏览器将该应用判定为「重要」从而避免节流。
已尝试的三种方案(效果不佳)
- 使用dash_devices:了解到它通过websocket而非HTTP请求进行更新,看似是可行方案,我在基础示例中实现了该方案,但尚未在大型应用中测试效果。
- 播放音频文件阻止标签页进入后台状态:从浏览器角度来看,播放音频(可设置低音量或关闭标签页声音)能让标签页保持前台状态。使用在线音频链接的代码片段可以正常运行:
但使用本地文件路径(如html.Audio(autoPlay=True, src='http://www.hochmuth.com/mp3/Haydn_Cello_Concerto_D-1.mp3', loop=True)src='/path/to/my_audio_file.mp3'或src='file:///path/to/my_audio_file.mp3')时无法正常播放音频,原因未知。我也尝试了base64编码方式,但仅在线链接的音频播放器可正常工作:
在四个音频播放器中,仅第一个可正常播放,其余均显示为灰色,仿佛未指定音频文件。注:base64方案是参考资料改编,可能存在实现错误;我知道这是临时解决方案,但至少部分有效。import dash from dash.dependencies import Input, Output import dash_html_components as html import dash_core_components as dcc import base64 haydn_path = "/Users/XXX/Haydn.mp3" encoded_haydn = base64.b64encode(open(haydn_path, 'rb').read()) app = dash.Dash(__name__) app.layout = html.Div([ html.Div("A div", id="my_div"), dcc.Interval(id='my_interval', disabled=False, n_intervals=0), html.Audio(autoPlay=True, src='http://www.hochmuth.com/mp3/Haydn_Cello_Concerto_D-1.mp3', loop=True), html.Audio(autoPlay=True, src=haydn_path, loop=True), html.Audio(autoPlay=True, src='file://' + haydn_path, loop=True), html.Audio(autoPlay=True, src='data:audio/mp3;base64,{}'.format(encoded_haydn), loop=True), ]) @app.callback(Output('my_div', 'children'), [Input('my_interval', 'n_intervals')]) def update(n_intervals): print(n_intervals) return dash.no_update if __name__ == '__main__': app.run_server(debug=True) - 将dcc.Interval组件置于层级结构的更高位置:有文档提到将
dcc.Interval组件置于更高层级可解决该问题,但我尝试后未取得效果。
我希望能得到以下任一方向的帮助:
- 找到让后台标签页中
dcc.Interval正常触发的方法; - 找到让
html.Audio组件播放本地存储音频文件的方法; - 找到其他可同时实现Dash应用与定期触发事件并关联二者的方案。
2021年4月13日更新:双线程分离架构方案
根据emher的回答,我转向了双线程分离架构:
- 一个线程使用
while True / time.sleep()循环定期采集串口外设数据,用纯Python编写,将采集结果存储在全局变量中; - 另一个线程运行Dash应用,通过
dcc.Interval定期读取上述全局变量。
以下是最小可运行示例:
import dash from dash.dependencies import Input, Output import dash_html_components as html import dash_core_components as dcc import threading import time counter = 0 app = dash.Dash(__name__) class DashThread(threading.Thread): def __init__(self, name): threading.Thread.__init__(self) self.name = name def run(self): global counter global app app.layout = html.Div([ dcc.Interval(id='my_interval', disabled=False, n_intervals=0), html.Div("Counter :", style={"display": "inline-block"}), html.Div(children=None, id="cnt_val", style={"display": "inline-block", "margin-left": "15px"}), ]) @app.callback(Output('cnt_val', 'children'), [Input('my_interval', 'n_intervals')]) def update(_): return counter app.run_server(dev_tools_silence_routes_logging=True) # , debug=True) class CountingThread(threading.Thread): def __init__(self, name): threading.Thread.__init__(self) self.name = name def run(self): global counter while True: counter += 1 print(counter) time.sleep(1) a = DashThread("The Dash Application") b = CountingThread("An Independent Thread") b.start() a.start() a.join() b.join()
注意事项
请注意代码中被注释的debug=True参数:
app.run_server(dev_tools_silence_routes_logging=True) # , debug=True)
这是因为Dash调用Flask的方式不允许在非主线程中启动Dash应用时开启调试模式,相关问题已有文档记录。
该脚本每秒计数一次,无论Dash应用是否在浏览器中加载或处于前台状态。只要脚本运行,计数就会持续进行,打开Dash应用标签页或切换到前台仅会更新计数器的显示。
潜在问题
与最初问题中的dcc.Interval方法不同,上述代码会逐渐出现不同步问题。使用dcc.Interval时,无论前一次callback是否完成,关联的callback都会每1.0秒触发一次。假设程序在t=0s启动且标签页处于前台,触发时间线为:
t=0s : callback()
t=1s : callback()
t=2s : callback()
t=3s : callback()
[...]
而使用多线程方案时,假设要执行的代码(如counter += 1 ; print(counter))的执行时间为dt,触发时间线为:
t=0s : callback()
t=1s + dt : callback()
t=2s + 2dt : callback()
t=3s + 3dt : callback()
[...]
即执行链会逐渐偏离预期的「每秒一次callback」的行为。在某些场景下这可能会造成问题,相关解决方法可参考相关资料。
内容的提问来源于stack exchange,提问作者mranvick

