Python & Tkinter -> 关于调用冻结程序的长时间运行函数

Posted

技术标签:

【中文标题】Python & Tkinter -> 关于调用冻结程序的长时间运行函数【英文标题】:Python & Tkinter -> About calling a long running function that freeze the program 【发布时间】:2012-06-07 06:52:28 【问题描述】:

我是 GUI 编程的新手,我正在尝试为我的一个 python 解析器制作一个 GUI。

我知道:

Tkinter 是单线程的。屏幕更新发生在事件循环的每次行程中。每当您有一个长时间运行的命令时,您都会阻止事件循环完成迭代,从而阻止事件处理,从而阻止重绘。

我的程序调用了一个大函数,需要大约 5 分钟才能完全运行。所以我想唯一的解决方案是你使用线程来执行长时间运行的命令。 但是,我长时间运行的命令已经线程化,所以我真的不知道如何继续。

--> 只要我在 GUI 中单击 BUT1,程序就会冻结,直到功能完全完成。我想在后台运行这个函数,这样程序就不会死机了。

--> 我不是在寻找一个完整的解决方案,但如果有人能让我走上正轨,那就太好了!

Main.py -> 图形用户界面 Module_1.py -> 我们通过点击按钮 BUT1 调用的函数

提前谢谢你!

这里是 Main.py --> 图形用户界面

#!/usr/bin/python
# -*- coding: utf-8 -*-


from Tkinter import *
import sys
import tkMessageBox
import tkFileDialog
import Module_1
import csv

from time import strftime, gmtime
DATE = strftime("_%d_%b_%Y")


class App:

    def __init__(self, master):

        self.frame = Frame(master, borderwidth=5, relief=RIDGE)
        self.frame.grid()

        class IORedirector(object):
            def __init__(self,TEXT_INFO):
                self.TEXT_INFO = TEXT_INFO

        class StdoutRedirector(IORedirector):
            def write(self,str):
                self.TEXT_INFO.config(text=self.TEXT_INFO.cget('text') + str)


        self.TEXT_HEADER = self.text_intro = Label(self.frame, bg="lightblue",text="THIS IS \n MY SUPER PROGRAM") 
        self.TEXT_HEADER.grid(row=0, column=0, columnspan=2, sticky=W+E+N+S)

        self.MENU = Frame(self.frame, borderwidth=5, relief=RIDGE, height=12) 
        self.MENU.grid(row=1, column=0, sticky=N)

        self.button = Button(self.MENU, text="QUIT", bg="red", command=self.frame.quit)
        self.button.grid(row=4, column=0)

        self.BUT1 = Button(self.MENU, text="BUT1", command=self.BUT1)
        self.BUT1.grid(row=0, column=0,sticky=W+E)



        self.TEXT_INFO = Label(self.frame, height=12, width=40, text="SOME TEXT", bg="grey",borderwidth=5, relief=RIDGE)
        self.TEXT_INFO.grid(row=1, column=1, sticky = N+W)

        sys.stdout = StdoutRedirector(self.TEXT_INFO)


    def BUT1(self):
        self.BUT1.config(text="RUNNING") 
        self.TEXT_INFO.config(text="BUT1 LAUNCHED")

        Module_1.main("BUT1")
        ## HERE WE NEED TO RUN THE FUNCTION
        ## THE PROGRAMM FREEZE HERE UNTIL THE FUNCTION IS ENTIRELY RUN

        self.TEXT_INFO.config(text="BUT1 FINISHED")
        self.BUT1.config(text="DONE")


root = Tk()
app = App(root)

root.mainloop()

这里是 Module_1.py --> 包含大函数

#!/usr/bin/python
# -*- coding: utf-8 -*-

import Queue
import threading
import urllib2
import time
from bs4 import BeautifulSoup as soup
from urllib2 import urlopen
import re
import os
import random
import sys
import logging
import csv
from time import strftime, gmtime
import os
import random
import shutil
import sys
import re
import logging
from threading import RLock
from time import strftime, gmtime
import csv
import urllib
from urllib import urlretrieve
from grab.spider import Spider, Task

logging.basicConfig(level=logging.CRITICAL) # Loggin to DEBUG / INFO
log = logging.getLogger()

DATE = strftime("_%d_%b_%Y")


class SPIDER1(Spider):
    initial_urls = ['URL_THAT_I_NEED_TO_PARSE']

    def __init__(self):
        super(SPIDER1, self).__init__(
            thread_number=20,
            network_try_limit=20,
            task_try_limit=20
        )
        self.result = 

    def task_initial(self, grab, task):
        for opt in grab.css_list("select[name='Template$TestCentreSearch1$SubRegionList'] option")[1:]:
            grab.set_input('Template$TestCentreSearch1$SubRegionList', opt.attrib['value'])
            grab.submit(extra_post=
                '__EVENTTARGET': 'Template$TestCentreSearch1$SubRegionList'
            , make_request=False)
            yield Task('parse', grab=grab, country=opt.text_content())

    def task_parse(self, grab, task):
        log.info('downloaded %s' % task.country)
        city_gen = (x.text_content() for x in grab.css_list(".TestCentreSearchLabel+br+span"))
        title_gen = (x.text_content() for x in grab.css_list(".TestCentreSearchTitle"))
        id_gen = (x.attrib['href'][-36:] for x in grab.css_list(".TestCentreSearchLink"))
        for x in zip(city_gen, title_gen, id_gen):
            self.result[x[2]] = 
                'country': task.country,
                'city': x[0],
                'name': x[1],
                'id': x[2],
                'price':'',
                'currency':'',
                'fee':''
            
            yield Task('info', 'URL_URL=%s' % x[2], id=x[2])

    def task_info(self, grab, task):
        for label in grab.css_list(".TestCentreViewLabel"):
            if label.text_content().strip()=="Test Fee:":
                fees = label.getnext().text_content().strip()
                self.result[task.id]['fee'] = fees
                price = re.findall('\d[\d\., ]+\d',fees)
                if price:
                    price = re.findall('\d[\d\., ]+\d',fees)[0]
                    self.result[task.id]['price'] = price.replace(' ','').replace(',','.')
                    currency = re.findall('[A-Z]2,3[$|€|£]?',fees)
                    if not currency:
                        currency = re.findall('[$|€|£]',fees)
                        if not currency:
                            currency = fees.replace(price,'').strip().replace('  ','')
                    if isinstance(currency,list):
                        currency = currency[0]
                    self.result[task.id]['currency'] = currency
                #log.info('      %(price)s   %(currency)s     -   %(fee)s ' % self.result[task.id])
                break


    def dump(self, path):
        """
        Save result as csv into the path
        """
        with open(path, 'w') as file:
            file.write("ID;Country;State;City;Name;Price;Currency;Original Fee\n")
            for test_center in sorted(self.result.values(), key=lambda x: "%(country)s%(city)s%(name)s" % x):
                file.write(("%(id)s;%(country)s;;%(country)s;%(name)s;%(price)s;%(currency)s;%(fee)s\n" % test_center).encode('utf8'))


def main(choice):
    parser, path, name = None, None, None

    def run(name,parser,path):
        log.info('Parsing %s...' % name)
        parser.run()
        parser.dump(path)
        log.info('Parsing %s completed, data was dumped into %s' % (name, path))
        log.info(parser.render_stats())


    if choice == "NONE":
        # DO NOTHING
        # HERE I'D LIKE TO HAVE ANOTHER CALL TO ANOTHER THREADED FUNCTION

    elif choice == "BUT1":
        run('Function1',SPIDER1(),'C:\LOL\Output1'+DATE+'.csv')

因此,通过单击 BUT1,我们运行 Module_1.py 文件中包含的 main("BUT1") 函数,该函数带有启动的参数 BUT1 -> run('Function1',SPIDER1(),'C:\LOL\Output1 '+日期+'.csv') 然后程序冻结直到解析器完成是工作.. :)

【问题讨论】:

【参考方案1】:

问题很简单:BUT1 不会返回,直到对 main 的调用返回。只要main(因此,BUT1)没有返回,你的 GUI 就会被冻结。

为此,您必须将main 放在单独的线程中。如果main 所做的只是等待其他线程,那么它还不足以产生其他线程。

【讨论】:

我试过这个但没有成功,我在线程方面很新,如果你只有一个简短的例子,它应该会很有帮助;) 有什么想法吗?我已经尝试过但没有任何成功......帮助会很有帮助;)【参考方案2】:

如果您偶尔从 BUT1 函数调用 root.update(),这应该可以防止 GUI 冻结。你也可以从一个具有固定间隔的 python 线程中做到这一点。

例如,每 0.1 秒更新一次:

from threading import Thread
from time import sleep

self.updateGUIThread = Thread(target=self.updateGUI)

def updateGUI(self):
    while self.updateNeeded
        root.update()
        sleep(0.1)

大函数完成后可以将self.updateNeeded设置为False。

【讨论】:

非常有趣!我正在尝试从 BUT1 函数调用 root.update() 并返回以将此线程关闭(如果它有效)!非常感谢,看来很有前途! @NokyamaShi:这非常危险。你真的试过这个吗?如果它起作用并且最终没有在某些时候导致死锁或崩溃,我会感到惊讶。调用update 通常是一种不好的做法,从另一个线程这样做似乎也相当危险。本质上,当您调用 update 时,您正在创建第二个事件循环,如果在此期间发生事件导致再次调用此代码,您可能会遇到真正的问题。 @BryanOakley:我自己用过这个没有任何问题,你能解释一下你的意思是什么“真正的问题”吗?这还不足以防止窗口变得无响应吗?另外,你有其他选择吗? @Junuxx:打电话给update 很危险。实际上,它启动了一个新的事件循环。如果在处理事件期间重新调用相同的代码,则最终会出现嵌套事件循环。每个事件循环都会阻塞,直到它的嵌套子级完成。另外,因为 Tkinter 不是线程安全的,所以从另一个线程调用 any tkinter 命令是有风险的。替代方案很简单,在另一个线程中运行耗时的过程,或者可以说更好,在另一个进程中。 我明白了,这是有道理的。但是,如果您确保更新间隔足够长,以便在再次调用之前有效地确定更新已完成,那会安全吗?

以上是关于Python & Tkinter -> 关于调用冻结程序的长时间运行函数的主要内容,如果未能解决你的问题,请参考以下文章

tkinter笔记:scale 尺度 (莫烦python笔记)

tkinter 笔记: radiobutton 选择按钮(莫烦python笔记)

python-tkinter学习

python-tkinter学习

tkinter 笔记:列表部件 listbox (莫烦python 笔记)

tkinter 笔记:创建输入框并显示结果 (莫烦python笔记)