【问题标题】:Can I import modules from thread in Python?我可以从 Python 中的线程导入模块吗?
【发布时间】:2017-10-11 23:03:57
【问题描述】:

这一切都始于昨晚,当时我正在制作一个需要 8 个左右包的脚本,其中包括 pygame.mixer,在我的计算机上导入这需要几秒钟。

这意味着在脚本开始之前,我必须等待 10 秒左右才能加载所有导入。因为我希望脚本显然尽可能快,所以我可以开始运行脚本,同时使用以下方式获取导入:

import threading


def import_modules():
    import tkinter as tk
    from pygame import mixer
    import json
    import webbrowser
    print('imports finished')

a = threading.Thread(target=import_modules)
a.start()
for i in range(10000):
    print('Getting Modules')

所以我的问题是:

这是否被认为是不好的做法,会导致问题吗?

如果是这样,我可以使用其他替代方案吗?

或者这样做可以吗?

【问题讨论】:

  • 虽然您可以在另一个线程中执行导入,但它可能无法达到您想要的效果。
  • @user2357112 为什么?它似乎确实加快了导入速度。
  • “开始运行脚本”是什么意思?如果脚本需要“8个左右的包”,如何在导入之前开始运行?
  • @user4815162342 就是这样。我可以通过导入正常方式获取最初需要的包,然后在脚本运行时获取其他包,当需要其中一个包时,它们将已经加载。

标签: python multithreading python-3.x


【解决方案1】:

如果您使用的是 CPython,这可能不会产生您预期的那么多改进。

CPython 有一个全局解释器锁(“GIL”),可确保一次只有一个线程可以执行 Python 字节码。

因此,每当导入线程正在执行 Python 代码时,其他线程都不会运行。 GIL 是由一个线程释放的,例如等待 I/O。因此会节省一些时间。

对于 tkinter 是否真正的线程安全存在不同意见。在原始线程中运行 tkinter 主循环仍然被认为是明智的,并且不要从其他线程调用 tkinter 调用,因为这可能导致崩溃。

GIL 也会导致 GUI 程序出现问题。如果您使用第二个线程进行长时间运行的计算,则用户界面可能会变得反应迟钝。至少有两种可能的解决方案。第一个是将长时间运行的计算分成小块,每个小块由after 方法执行。第二种是在不同的进程中运行计算。


来自 cmets 的后续问题:

还有什么可以加快执行时间的吗?

你必须做的第一件事是测量究竟是什么导致了问题。然后您可以查看问题区域并尝试改进它们。

例如模块加载时间。在profiler 下运行您的应用,查看模块加载需要多长时间以及原因。

如果pygame.mixer 加载时间过长,您可以使用您平台的本机混音器。类 UNIX 操作系统通常有一个/dev/mixer 设备,而 ms-windows 有不同的 API。使用这些绝对不会花费 10 秒。 与此相关的成本是:您将失去操作系统之间的可移植性。

有什么选择

使用多核是尝试加快速度的常用策略。目前在 CPython 上,让代码在多个内核上并行运行的唯一通用方法是使用 multiprocessingconcurrent.futures

但是,如果这种策略有效,则取决于您的问题的性质。

如果您的问题涉及对大量数据执行相同的计算,那么并行化相对容易。在这种情况下,您可以预期最大加速大致相当于您使用的内核数量。

您的问题可能由多个步骤组成,每个步骤都取决于上一步的结果。此类问题本质上是串行的,并且更难并行执行。

其他可能加快速度的方法可能是使用另一个 Python 实现,例如 Pypy。或者,您可以使用 cython 和类型提示将性能关键部分转换为已编译的 C 代码。

【讨论】:

  • 在 GIL 之外,还有一个 global lock protecting module imports as well,因此线程导入毫无意义。
  • @ShadowRanger 在当前的 Python 3 版本中仍然是这种情况吗?在 3.1 和 3.6 之间的某个地方,线程代码中提到的导入似乎已经从 Python 文档的线程章节中消失了。
  • Ok 这对 Tkinter 来说似乎是个坏主意,其他模块(不是 GUI)会受到影响吗?有哪些替代方案(如果有) 除了标准库调用之外的所有内容都可以是 pyc 文件 还有什么可以加快执行时间吗?
  • @RolandSmith:啊,是的,忘了in 3.3 they introduced per module import locks。所以只是被 GIL 阻止了;只有在多个线程中导入同一个模块时,导入锁才会成为问题(这可能比您想象的更容易,因为大多数导入都会触发依赖模块的导入)。
  • @Simon 这可能是您的操作系统缓存预热的影响。今天的大多数操作系统都使用空闲内存作为磁盘缓存,因为磁盘比 RAM 慢得多。经过几次尝试,您的所有模块文件都将在该磁盘缓存中,可以更快地访问它们。 :-)如果您要重新启动 PC,您可能会再次看到相同的内容。操作系统也有其他影响。普遍的共识似乎是 ms-windows 上的磁盘操作明显慢于例如。 Linux。
【解决方案2】:

我知道这是一个旧线程,但我正在寻找一种方法来最小化我的应用程序的加载时间,并希望用户看到 gui,以便他可以在后台导入其他模块时与之交互

我已经阅读了一些建议惰性导入技术的答案,我发现“对我来说”很复杂,然后我偶然发现这里建议使用线程在后台导入模块,然后我试了一下,发现了是最适合我需要的绝妙主意

下面是一个使用PySimpleGUI的示例gui应用程序的代码,它要求用户输入一个url,它将在默认浏览器窗口中打开它,唯一需要这样做的模块是webbrowser,所以这个工作可以在加载其他模块时完成

我在这段代码中添加了 cmets 来解释所有部分,希望它对某人有所帮助, 在 python 3.6、windows10 上测试。

请注意:这只是一个演示代码。

# import essentials first
import PySimpleGUI as sg
import time, threading

# global variable names to reference to the imported modules, this way will 
# solve the problem of importing inside a function local namespace
pg = None
js = None
wb = None

progress = 0  # for our progress bar

def importer():
    # we will simulate a time consuming modules by time.sleep()
    global progress
    progress = 10
    start = time.time()
    global pg, js, wb
    import pygame as pg
    time.sleep(3)
    print(f'done importing pygame mixer in {time.time()-start} seconds')
    progress = 40

    start = time.time()
    import webbrowser as wb
    time.sleep(2)
    print(f'done importing webbrowser in {time.time()-start} seconds')
    progress = 70

    start = time.time()
    import json as js
    time.sleep(10)
    print(f'done importing json in {time.time()-start} seconds')

    progress = 100
    print('imports finished')

# start our importer in a separate thread
threading.Thread(target=importer).start()

# main app 
def main():
    # window layout
    layout = [[sg.Text('Enter url:', size=(15,1)), sg.Input(default_text='https://google.com', size=(31, 1), key='url')],
            [sg.Text('Loading modules:', size=(15,1), key='status'), 
            sg.ProgressBar(max_value=100, orientation='horizontal', size=(20,10), key='progress')],
            [sg.Button('Open url', disabled=True, key='open_url'), sg.Button('joysticks', disabled=True, key='joysticks'), sg.Cancel()]]
    window = sg.Window('test application for lazy imports', layout=layout) # our window

    while True:  # main application loop
        event, values = window.Read(timeout=10)  # non blocking read from our gui
        if event in [None, 'Cancel']:
            window.Close()
            break 

        elif event == 'open_url':
            wb.open(values['url'])
            print('helllooooooooooooo')

        elif event == 'joysticks':
            # show joystics number currently connected
            pg.init()
            n = pg.joystick.get_count()  # Get count of joysticks
            sg.Popup(f'joysticks number currently connected to computer = {n}')

        # open url button is disabled by default and will be enabled once our webbrowser module imported
        if wb:
            window.Element('open_url').Update(disabled= False)

        if pg:
            window.Element('joysticks').Update(disabled= False)

        # progress bar
        window.Element('progress').UpdateBar(progress)
        if progress >= 100:
            window.Element('status').Update('Loading completed', background_color='green')

main()

【讨论】:

  • 我喜欢!非常聪明!我仍然不是 Python 方面的专家,所以不知道它是否是最好的解决方案,但我认为这是一个好主意,也许可以很好地工作。该代码在我的机器上运行良好。
  • 我的应用程序启动速度提高了 50%。现在的问题是为什么内置导入器还没有进行这种优化?
  • 很高兴能帮到你,祝你好运
猜你喜欢
  • 2013-01-08
  • 2019-12-22
  • 1970-01-01
  • 1970-01-01
  • 2022-12-20
  • 1970-01-01
  • 1970-01-01
  • 2023-03-26
  • 2023-04-03
相关资源
最近更新 更多