【问题标题】:How can threading be used to create multiple independent re-usable object instances?如何使用线程来创建多个独立的可重用对象实例?
【发布时间】:2015-07-01 07:20:34
【问题描述】:

我正在用 Python 构建一个 GUI,并使用线程来生成多个独立工作的倒数计时器。我有一个用于测试的最小化模块,我将其包括在下面。我希望按钮开始倒计时,然后当再次单击它时停止它,然后重置以便可以在下次单击时启动它。一旦时间用完,也会自行重置。唯一的问题是它停止后,我无法让它重新启动。我收到“无法启动线程两次”错误。我一直在尝试使用条件循环让线程退出其自身,但它一直没有工作。我真的很感激一些见解

实际上我希望这个程序能够做两件事。 1) 一直跑完定时器然后自动复位,这样就可以重新启动了 2) 在倒计时中间停止,让它自动重置以便重新启动

我认为解决这些问题对于社区来说是有价值的,因为它是解决论坛上很多人谈论的问题的现实世界解决方案的一个例子,即如何解决没有重启线程的问题.

__author__ = 'iKRUSTY'

'''
there are two things i would like this code to be able to do
1) run all the way through the timer and then reset so that it can be restarted
2) be stopped before completing and then reset so that it can be restarted

'''

from tkinter import *
import time
import os
import threading

#Variables
global FRYER_ONE_TIME_VAR       # holds the value for changing label text to update timer
global BASKET_ONE_TARGET_TIME       #this is a user input that will determine the length of the countdown
global timerOneStayAlive        #this is the value that i am attempting to use so that the thread closes after it is set to false
timerOneStayAlive = FALSE       #initializes to false because the the invert function changes it to true once the button is clicked

FRYER_ONE_TIME_VAR=" "        #used to pass time between functiuons

#Font Profiles
SMALLEST_FONT = ("Verdana", 9)
SMALL_FONT = ("Verdana", 10)
LARGE_FONT = ("Verdana", 12)
LARGEST_FONT = ("Verdana", 18)

class timer():
    global BASKET_ONE_TARGET_TIME
    BASKET_ONE_TARGET_TIME = 5      #Just setting it manually for now

    def __init__(self):
        self.s = 0      #these values are used to convert from seconds to a minute:second format
        self.m = 0       #these values are used to convert from seconds to a minute:second format

    def SetTime(self, seconds):
        self.seconds=seconds        #this is a counter that will be used to calculate remaining time

    def TimerReset(self):
        self.seconds = BASKET_ONE_TARGET_TIME       #resets counter to target time

    def StartCountdown(self, FryerLabel): #takes a label as an argumet to tell it where to display the countdown

        global timerOneStayAlive
        print("StartCountdown Started!")        #USED FOR TROUBLE SHOOTING
        self.seconds = BASKET_ONE_TARGET_TIME   #set start value for seconds counter
        self.seconds=self.seconds+1      #makes the full time appear upon countdown start

        while self.seconds > 0:
            FRYER_ONE_TIME_VAR = self.CalculateTime()       #Calculate time reduces the counter by one and reformats it to a minute:second format. returns a string
            FryerLabel.config(text=FRYER_ONE_TIME_VAR)      #Update Label with current value
            print(self.seconds)                               #USED FOR TROUBLE SHOOTING
            time.sleep(1)
            # reset label with default time
            if self.seconds == 0:                           #Reset once counter hits zero
                print("resetting time")                       #USED FOR TROUBLE SHOOTING
                self.seconds = BASKET_ONE_TARGET_TIME + 1
                FRYER_ONE_TIME_VAR = self.CalculateTime()
                FryerLabel.config(text=FRYER_ONE_TIME_VAR)
                break
        print("TimerStayAlive before invert: ", timerOneStayAlive)          #USED FOR TROUBLE SHOOTING
        timerOneStayAlive = invert(timerOneStayAlive)                   #inverts the value back to FALSE so that Ideally the loop breaks
                                                                        #  and the thread completes so that it can be called again
        print("TimerStayAlive after invert: ", timerOneStayAlive)       #USED FOR TROUBLE SHOOTING
        print("end startcountdown")                                 #USED FOR TROUBLE SHOOTING

    def CalculateTime(self):
        #print("CalculateTime has been called")
        lessThanTen=0
        self.seconds = self.seconds - 1
        self.m, self.s = divmod(self.seconds, 60)

        if self.s<10:
            lessThanTen=1
        #create time String Variables
        colonVar=':'
        minutesString = str(self.m)
        secondsString = str(self.s)
        #insert variables into string array
        timeArray = []
        timeArray.append(minutesString)
        timeArray.append(colonVar)
        if lessThanTen == 1:
            timeArray.append("0")
        timeArray.append(secondsString)
        #prepare for output
        self.timeString = ''.join(timeArray)
        return self.timeString

def invert(boolean):
    return not boolean

def BasketOneButtonClicked():
    print("button clicked")             #USED FOR TROUBLE SHOOTING
    global timerOneStayAlive
    timerOneStayAlive = invert(timerOneStayAlive)   #Changes from FALSE to TRUE
    if timerOneStayAlive == TRUE:
        print("timerOneStayAlive: ", timerOneStayAlive)         #USED FOR TROUBLE SHOOTING
        basketOneThread.start()
        updateButtonStatus()                                #changes text of button depending on whether timerOneStayAlive is TRUE or FALSE
    else:
        print("timerOneStayAlive: ", timerOneStayAlive)     #USED FOR TROUBLE SHOOTING
        return


def updateButtonStatus():
    global timerOneStayAlive
    if timerOneStayAlive == FALSE:
        basketOneStartButton.config(text="Start")
    if timerOneStayAlive == TRUE:
        basketOneStartButton.config(text="Stop")


def BasketOneThreadComShell():          # I used this so that i can ideally call multiple functions with a single thread
    '''
    This is where i think the problem may be. this is what is called when the thread is initialized and theoretically
    when this completes the thread should come to a halt so that when the button is reset, the thread can be called again
    I think that the function is completing but for some reason the thread keeps on running.
    '''

    global timerOneStayAlive
    print("ComShell Started")       #USED FOR TROUBLE SHOOTING
    while timerOneStayAlive:
        basketOneTimer.StartCountdown(countdownLabelBasket1)
        updateButtonStatus()
        if timerOneStayAlive == FALSE:      #redundant because while loop should do it. i just tried it because i couldnt get the process to end
            break
    print("Threadshell has ended")      #USED FOR TROUBLE SHOOTING
    return
    print("after return check")     #USED FOR TROUBLE SHOOTING



root = Tk()

'''
the  following is all just building the GUI Stuff
'''

Container = Frame(root)
Container.grid(row=1, column=0, padx=10, pady=10)
countdownContainerBasket1 = Label(Container, width=10, height=5)
countdownContainerBasket1.grid(row=2, column=0, sticky=NSEW)
countdownLabelBasket1 = Label(countdownContainerBasket1, text="Basket 1", background="white", anchor=CENTER, width=10, height=6, font=LARGE_FONT, padx=20)
countdownLabelBasket1.pack()
basketOneTimer = timer()
basketOneTimer.SetTime(5)
basketOneStartButton = Button(Container, text="Start", font=LARGE_FONT, command=BasketOneButtonClicked)
basketOneStartButton.grid(row=3, column=0, padx=10, pady=10)

basketOneThread = threading.Thread(target=BasketOneThreadComShell) #this is where the thread is initialized. start() is called in BasketOneButtonClick

print(threading.active_count)       #tried to use this to see if the thread was exiting but it didnt help

root.mainloop()

您可能想试一试,看看 GUI 是什么样子,以及我说的计时器是什么意思。尝试让它一直运行并重置以了解我在说什么。

如果有任何其他有用的信息,请告诉我,请记住,这是我使用 python 的第一周,所以我不是专家。谢谢大家

【问题讨论】:

  • 如果您所做的只是一个倒数计时器,那么您就不需要线程——它们增加了复杂性而没有增加任何实际价值。
  • 这就是每个人都在说的,但没有线程我怎么能有 4 个计时器,它们基本上只是延迟 1 秒的倒计时循环。彼此独立运行,更新 GUI 并在 Ras-P? 上驱动 GPIO。当我运行计时器时,除非它在单独的线程中,否则它不会锁定直到倒计时循环完成?如果我不使用线程,即使按钮锁定,我也无法再次按下它来取消。我已经让它与线程一起工作,它一点也不复杂,但它只是令人沮丧的是没有命令来结束线程。如果你能命名它,你应该能够杀死它
  • 请参阅stackoverflow.com/q/2400262/7432 了解不使用线程的计时器示例。可以很容易地推断该示例以保留任意数量的计时器(嗯,可能多达几百个)。
  • 谢谢布赖恩,我想我只能走这条路了。你很有帮助。
  • 刚刚使用 after()。起初不想,但这绝对是电话。再次感谢您的帮助!

标签: python multithreading timer tkinter


【解决方案1】:

python 社区很幸运有一个被广泛接受的风格指南https://www.python.org/dev/peps/pep-0008/。由于您的非 pep8 命名和格式,我很难快速理解您所说的问题。

其他线程无法与您的小部件交互,只有 tkinter 主循环可以做到这一点。您可以使用 tkinter 提供的 after 方法在主循环中的给定时间段后运行函数。

看这个例子 以Mutli-threading python with Tkinter 为例!

需要明确的是,使用 tkinter 进行线程化可能有效,但它不可靠,并且您会遇到难以调试的意外行为。不要这样做。

建议

  1. 将调用 tkinter after 方法来启动一个函数,该函数无限期地检查队列以查找要运行的函数。然后它使用 after 方法安排这些函数由 tkinter 运行

  2. 我会有一个带有启动、停止和暂停方法的计时器类。 启动时,它会向上/向下计数,并在完成时简单地重新启动。您创建的每个计时器实例还需要对 tkinter 按钮的引用。

  3. 要更新按钮,您可以在队列中放置一个看起来像这样的函数

    tkinter_queue.put(lambda: but.config(text=i))
    

现在检查队列的功能将更新您的按钮计时器

同样,当您处于 Python 编程的第一周时,您可以确定您遇到的大多数问题都会在这里找到答案,如果不是在其他地方。请事先做好研究。 Gui 编程和线程不是最简单的主题,因此更有理由进行研究。

【讨论】:

  • 感谢您的帮助!我向你保证,你在这里和外面都做了相当多的清洗。我发现的大多数信息都与您的回复类似。我希望能够了解为什么退出的while循环方法不起作用,这不是我见过的讨论的主题。但是大多数人只是警告说线程很难,你可能不应该这样做。这似乎太方便了,太诱人了。顺便感谢 pep-0008 链接,它真的很有帮助,我一定会在将来应用它。如果您还有其他想法,请告诉我
  • 没问题,线程可能是一个棘手的话题,所以我建议您再问一个问题,提供一个说明问题所在的最小示例。如果您分解代码,也将更容易调试和隔离问题。您仍然可以使用线程,但您只需将任何修改 tkinter 的代码放入队列中。
  • 我刚刚使用了 after()。像魅力一样工作。所以放心了。感谢您的帮助和建设性的指导。
猜你喜欢
  • 2015-05-25
  • 1970-01-01
  • 1970-01-01
  • 2021-11-26
  • 1970-01-01
  • 2020-10-11
  • 2019-12-28
  • 2012-11-17
  • 1970-01-01
相关资源
最近更新 更多