【问题标题】:Intermittent Python thread error, "main thread is not in main loop"间歇性 Python 线程错误,“主线程不在主循环中”
【发布时间】:2014-08-17 16:59:49
【问题描述】:

中年父亲(电气工程师不是专业程序员)试图教我 13 岁的女儿电子和编程。到目前为止,我喜欢 Python。我正在构建一个程序,用 tkinter GUI 和 DS18B20 传感器显示我们房子的温度。

我们通过阅读书籍、在线研究和使用 Stack Overflow 解决错误(这个网站太棒了!)拼凑出以下程序。

现在我们很困惑,我们不断收到间歇性错误,当我们在 Raspberry 上加载空闲后第一次运行程序时,它运行正常。

第二次以及之后的所有时间,我们都会收到以下错误消息:

Traceback (most recent call last):
  File "/home/pi/Code-working-library/stackoverflow-paste.py", line 140, in <module>
    app.equipTemp.set(tempread)
  File "/usr/lib/python2.7/lib-tk/Tkinter.py", line 203, in set
    return self._tk.globalsetvar(self._name, value)
RuntimeError: main thread is not in main loop

注意,我们的理解是,为了有一个静态窗口和更新标签,更新的温度从我们的传感器 (DS18B20) 中读取,我们需要使用一个线程。我们开始的示例代码有 _init_ 语句,前后只有一个下划线 - 不知道为什么,如果我添加第二个下划线,我会收到错误消息。我们作为基础的更新窗口代码来自Raspberry Pi forum

这是我们的代码:

from Tkinter import *
import tkFont
import os
import glob
import time
import subprocess
import re
import sys
import time
import threading
import Image 
import ImageTk

os.system('modprobe w1-gpio')
os.system('modprobe w1-therm')

#28-000005c6ba08

sensors = ['28-000005c6ba08'] 
sensors1 = ['28-000005c70f69'] 

def read_temp_raw():
    catdata = subprocess.Popen(['cat',device_file], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out,err = catdata.communicate()
    out_decode = out.decode('utf-8')
    lines = out_decode.split('\n')
    return lines

def read_temp():
    lines = read_temp_raw()
    while lines[0].strip()[-3:] != 'YES':
        time.sleep(0.2)
        lines = read_temp_raw()
    equals_pos = lines[1].find('t=')
    if equals_pos != -1:
        temp_string = lines[1][equals_pos+2:]
        temp_c = float(temp_string) / 1000.0
        temp_f = temp_c * 9.0 / 5.0 + 32.0
        return temp_f

###########  build window  ###################

bground="grey"


class App(threading.Thread):

    def _init_(self):    
        threading.Thread._init_(self)
        self.start()

    def callback(self):
        self.root.quit()        


    def run(self):

        #Make the window
        self.root = Tk() 
        self.root.wm_title("Home Management System")
        self.root.minsize(1440,1000)

        self.equipTemp = StringVar()   
        self.equipTemp1 = StringVar()
        self.equipTemp2 = StringVar()       

        self.customFont = tkFont.Font(family="Helvetica", size=16)

        #   1st floor Image
        img = Image.open("HOUSE-PLANS-01.png") 
        photo = ImageTk.PhotoImage(img)

        Label1=Label(self.root, image=photo)
        Label1.place(x=100, y=100)

        #   2nd floor
        img2 = Image.open("HOUSE-PLANS-02.png")
        photo2 = ImageTk.PhotoImage(img2)

        Label1=Label(self.root, image=photo2)
        Label1.place(x=600, y=100)

        #   Basement image
        img3 = Image.open("HOUSE-PLANS-03.png")
        photo3 = ImageTk.PhotoImage(img3)

        Label1=Label(self.root, image=photo3)
        Label1.place(x=100, y=500)

        #   Attic Image
        img4 = Image.open("HOUSE-PLANS-04.png")
        photo4 = ImageTk.PhotoImage(img4)

        Label1=Label(self.root, image=photo4)
        Label1.place(x=600, y=500)

        #   House Isometric Image
        img5 = Image.open("house-iso.png")
        photo5 = ImageTk.PhotoImage(img5)

        Label1=Label(self.root, image=photo5)
        Label1.place(x=1080, y=130)

        #Garage Temp Label
        Label2=Label(self.root, textvariable=self.equipTemp, width=6, justify=RIGHT, font=self.customFont)
        Label2.place(x=315, y=265)



        print "start monitoring and updating the GUI"

        self.root.mainloop() #start monitoring and updating the GUI



###########  Start Loop    ###################

print "starting app"

app = App()
app.start()

print "app started"


###################  Begin ds18b20 function  ##############

while True:

    #   28-000005c6ba08
    i = "28-000005c6ba08"
    base_dir = '/sys/bus/w1/devices/'
    device_folder = glob.glob(base_dir + i)[0]
    device_file = device_folder + '/w1_slave'

    tempread=round(read_temp(),1)


    app.equipTemp.set(tempread)
    time.sleep(5)

    ##################### END ds18b20 Function  ######

【问题讨论】:

  • 对 tkinter 做的不多,但 gui 工具包通常只在主线程上运行。显而易见的解决方案:将主循环留在主线程上,然后将计算放到另一个线程中!
  • 是的,将您的计算放在 App 类中。
  • 感谢您的快速响应,但请把我当作白痴对待。当您说将我的计算放在应用程序类中时,您是指注释后的代码:## Begin ds18b20 function ## 还是代码开头的两个 read temp 函数?
  • 现在你在主线程中进行计算(Begin ds18b20 函数之后的东西),你的 gui 在一个额外的线程中。你应该反过来做!
  • 顺便说一句,您绝对应该使用__init__,而不是_init_。您在使用 __init__ 时遇到的任何错误可能是因为您犯了一个合法的编码错误,该错误仅在实际调用 __init__ 时发生。

标签: python multithreading raspberry-pi


【解决方案1】:

您需要在主线程中运行 GUI 代码,而您的温度读取代码需要在后台线程中。只有在主线程中更新 GUI 才是安全的,因此您可以通过 Queue 将您从后台线程读取的温度数据传递回主线程,并让主线程定期检查队列中的数据使用self.root.after():

from Tkinter import *
import tkFont
import os
import glob
import time
import threading
import Image 
import Queue


def update_temp(queue):
    """ Read the temp data. This runs in a background thread. """
    while True:
        #   28-000005c6ba08
        i = "28-000005c6ba08"
        base_dir = '/sys/bus/w1/devices/'
        device_folder = glob.glob(base_dir + i)[0]
        device_file = device_folder + '/w1_slave'

        tempread=round(read_temp(),1)

        # Pass the temp back to the main thread.
        queue.put(tempread)
        time.sleep(5)

class Gui(object):
    def __init__(self, queue):
        self.queue = queue

        #Make the window
        self.root = Tk() 
        self.root.wm_title("Home Management System")
        self.root.minsize(1440,1000)

        self.equipTemp = StringVar()   
        self.equipTemp1 = StringVar()
        self.equipTemp2 = StringVar()       

        self.customFont = tkFont.Font(family="Helvetica", size=16)

        #   1st floor Image
        img = Image.open("HOUSE-PLANS-01.png") 
        photo = ImageTk.PhotoImage(img)

        Label1=Label(self.root, image=photo)
        Label1.place(x=100, y=100)

        #   2nd floor
        img2 = Image.open("HOUSE-PLANS-02.png")
        photo2 = ImageTk.PhotoImage(img2)

        Label1=Label(self.root, image=photo2)
        Label1.place(x=600, y=100)

        #   Basement image
        img3 = Image.open("HOUSE-PLANS-03.png")
        photo3 = ImageTk.PhotoImage(img3)

        Label1=Label(self.root, image=photo3)
        Label1.place(x=100, y=500)

        #   Attic Image
        img4 = Image.open("HOUSE-PLANS-04.png")
        photo4 = ImageTk.PhotoImage(img4)

        Label1=Label(self.root, image=photo4)
        Label1.place(x=600, y=500)

        #   House Isometric Image
        img5 = Image.open("house-iso.png")
        photo5 = ImageTk.PhotoImage(img5)

        Label1=Label(self.root, image=photo5)
        Label1.place(x=1080, y=130)

        #Garage Temp Label
        Label2=Label(self.root, textvariable=self.equipTemp, width=6, justify=RIGHT, font=self.customFont)
        Label2.place(x=315, y=265)

        print "start monitoring and updating the GUI"

        # Schedule read_queue to run in the main thread in one second.
        self.root.after(1000, self.read_queue)

    def read_queue(self):
        """ Check for updated temp data"""
        try:
            temp = self.queue.get_nowait()
            self.equipTemp.set(temp)
        except Queue.Empty:
            # It's ok if there's no data to read.
            # We'll just check again later.
            pass
        # Schedule read_queue again in one second.
        self.root.after(1000, self.read_queue)

if __name__ == "__main__":
    queue = Queue.Queue()
    # Start background thread to get temp data
    t = threading.Thread(target=update_temp, args=(queue,))
    t.start()
    print "starting app"
    # Build GUI object
    gui = Gui(queue)
    # Start mainloop
    gui.root.mainloop()

编辑:

在实际查看了 tkinter 源代码以及 Python 错误跟踪器之后,似乎与几乎所有其他 GUI 库不同,tkinter 旨在实现线程安全,只要您在应用程序的主线程中运行主循环。有关更多信息,请参阅我添加的答案 here,或直接转到 Python 错误跟踪器 here 上已解决的关于 tkinter 线程安全的问题。如果 tkinter 源代码和 Python 的错误跟踪器是正确的,这意味着只要您在主线程中运行 mainloop,您就可以愉快地直接从温度读取线程中调用 gui.equipTemp.set() - 不需要 Queue。在我的测试中,这确实工作得很好。

【讨论】:

    【解决方案2】:

    GUI 工具包不是线程安全的。您只能从主线程构建和更改您的 GUI。 由于读取温度不需要那么长时间,您可以删除所有线程代码并使用 Tk 中的after-方法。

    你的read_temp_raw函数很复杂:

    def read_temp_raw():
        with open(device_file) as temp:
            return temp.read().split('\n')
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-19
      • 1970-01-01
      • 2019-03-21
      • 1970-01-01
      相关资源
      最近更新 更多