Flask-socketio 在后台线程中复制文件时错过事件

Flask-socketio misses events while copying file in background thread

(在 github 上完成测试应用:https://github.com/olingerc/socketio-copy-large-file

我将 Flask 与 Flask-SocketIO 插件一起使用。我的客户可以要求服务器通过 websocket 复制文件,但是在复制文件时,我希望客户能够与服务器通信以要求它做其他事情。我的解决方案是 运行 后台线程中的复制过程 (shutil)。这是函数:

def copy_large_file():
    source = "/home/christophe/Desktop/largefile"
    destination = "/home/christophe/Desktop/largefile2"
    try:
        os.remove(destination)
    except:
        pass
    print("Before copy")
    socketio.emit('my_response',
                  {'data': 'Thread says: before'}, namespace='/test')
    shutil.copy(source, destination)
    print("After copy")
    socketio.emit('my_response',
                  {'data': 'Thread says: after'}, namespace='/test')

我观察到以下行为: 使用原生 socketio 方法启动函数时:

socketio.start_background_task(target=copy_large_file)

复制大文件时所有传入事件都会延迟,直到文件完成并开始下一个文件。我猜想 shutil 没有释放 GIL 或类似的东西,所以我用线程测试了:

thread = threading.Thread(target=copy_large_file)
thread.start()

同样的行为。也许是多处理?

thread = multiprocessing.Process(target=copy_large_file)
thread.start()

啊!这有效,并且正确接收了 copy_large_file 函数中通过 socketio 发出的信号。 但: 如果用户开始复制一个非常大的文件,关闭浏览器并在 2 分钟后返回,套接字不再连接到同一个 socketio "session?",因此不再接收后台进程发出的消息。

我想主要问题是:如何在不阻塞 flask-socketio 的情况下在后台复制大文件,但仍然能够从后台进程中向客户端发出信号。

测试应用可用于重现该行为:

在浏览器中:

您在问两个不同的问题。

首先,我们来讨论文件的实际复制。

您似乎正在为您的服务器使用 eventlet。虽然此框架为网络 I/O 功能提供了异步替换,但磁盘 I/O 以非阻塞方式执行起来要复杂得多,特别是在 Linux 上(有关问题的一些信息 here).因此,正如您所注意到的,即使在标准库猴子修补的情况下,对文件执行 I/O 也会导致阻塞。顺便说一句,这与gevent相同。

对文件执行非阻塞 I/O 的典型解决方案是使用线程池。使用 eventlet,eventlet.tpool.execute 函数可以做到这一点。所以基本上,不是直接调用 copy_large_file(),而是调用 tpool.execute(copy_large_file)。这将使应用程序中的其他绿色线程能够 运行 而复制发生在另一个系统线程中。顺便说一句,您使用另一个进程的解决方案也是有效的,但根据您需要执行其中一个副本的次数和频率,它可能有点矫枉过正。

您的第二个问题与 "remembering" 启动长文件复制的客户端有关,即使浏览器关闭并重新打开也是如此。

这确实是您的应用程序需要通过存储恢复返回客户端所需的状态来处理的事情。大概您的客户有办法通过令牌或其他一些标识来识别您的应用程序。当服务器启动这些文件副本之一时,它可以为操作分配一个 ID,并将该 ID 存储在与请求它的客户端关联的数据库中。如果客户端离开然后 returns,您可以查找是否有任何正在进行的文件副本,这样可以将客户端同步回关闭浏览器之前的状态。

希望对您有所帮助!