TkInter越来越多地占用内存

huangapple go评论66阅读模式
英文:

TkInter is hoarding more and more ram

问题

Sure, here are the translated code parts as you requested:

Original Code:

import subprocess
import time
import tkinter as tk
import threading
import gc

ip_addresses = ["192.168.1.83", "192.168.1.93", "192.168.1.93", "192.168.1.92", "192.168.1.95", "192.168.1.103", "192.168.1.105"]
i= 0
if i == 0:
    root = tk.Tk()
    root.title("Online")
    root.geometry("500x300")
    i+1

label_texts = [f"{ip_address} -> unknown" for ip_address in ip_addresses]
labels = [tk.Label(root, text=label_text, font=("Arial", 16), fg="#FF0000") for label_text in label_texts]
for i, label in enumerate labels:
    label.pack(padx=10, pady=5)

def check_ip_status(ip_address, label):
    try:
        subprocess.check_output(f"ping {ip_address} -n 1", shell=True, timeout=5)
        label.config(text=f"{ip_address} -> online", fg="#00FF00") # Green
    except subprocess.CalledProcessError:
        label.config(text=f"{ip_address} -> offline", fg="#FF0000") # Red
    except subprocess.TimeoutExpired:
        label.config(text=f"{ip_address} -> offline (timeout)", fg="#FFA500") # Orange

def check_all_ip_statuses():
    processes = []
    for i, ip_address in enumerate(ip_addresses):
        label = labels[i]
        process = subprocess.Popen(f"ping {ip_address} -t", shell=True, stdout=subprocess.DEVNULL)
        processes.append(process)
        thread = threading.Thread(target=check_ip_status, args=(ip_address, label))
        thread.start()

    root.after(5000, check_all_ip_statuses) 
    gc.collect() 

    # Close all subprocesses
    for process in processes:
        process.kill()
        process.communicate()

check_all_ip_statuses()
root.mainloop()

Updated Code:

import pythonping
import time
import tkinter as tk
import threading
import gc

ip_addresses = ["192.168.1.83", "192.168.1.85", "192.168.1.93", "192.168.1.92", "192.168.1.95", "192.168.1.103", "192.168.1.105"]

i= 0
if i == 0:
    root = tk.Tk()
    root.title("Online")
    root.geometry("500x300")
    i+1

def create_labels():
    global labels
    labels = [tk.Label(root, text=f"{ip_address} -> unknown", font=("Arial", 16), fg="#FF0000") for ip_address in ip_addresses]
    for label in labels:
        label.pack(padx=10, pady=5)

def check_ip_status(ip_address, label):
    response = pythonping.ping(ip_address, count=1, timeout=5)
    if response.success():
        label.config(text=f"{ip_address} -> online", fg="#00FF00") # Green
    else:
        label.config(text=f"{ip_address} -> offline", fg="#FF0000") # Red

def check_all_ip_statuses():
    for i, ip_address in enumerate(ip_addresses):
        label = labels[i]
        thread = threading.Thread(target=check_ip_status, args=(ip_address, label))
        thread.start()

    gc.collect() 
    root.after(1000, check_all_ip_statuses) 

create_labels()
check_all_ip_statuses()
root.mainloop()
英文:

I am creating a script to check the status of some of my computers but noticed that over time the Tkinter window is accumulating more and more resources. Is there any way to fix this?

import subprocess
import time
import tkinter as tk
import threading
import gc
ip_addresses = ["192.168.1.83", "192.168.1.93", "192.168.1.93", "192.168.1.92", "192.168.1.95", "192.168.1.103", "192.168.1.105"]
i= 0
if i == 0:
root = tk.Tk()
root.title("Online")
root.geometry("500x300")
i+1
label_texts = [f"{ip_address} -> unknown" for ip_address in ip_addresses]
labels = [tk.Label(root, text=label_text, font=("Arial", 16), fg="#FF0000") for label_text in label_texts]
for i, label in enumerate(labels):
label.pack(padx=10, pady=5)
def check_ip_status(ip_address, label):
try:
subprocess.check_output(f"ping {ip_address} -n 1", shell=True, timeout=5)
label.config(text=f"{ip_address} -> online", fg="#00FF00") # Green
except subprocess.CalledProcessError:
label.config(text=f"{ip_address} -> offline", fg="#FF0000") # Red
except subprocess.TimeoutExpired:
label.config(text=f"{ip_address} -> offline (timeout)", fg="#FFA500") # Orange
def check_all_ip_statuses():
processes = []
for i, ip_address in enumerate(ip_addresses):
label = labels[i]
process = subprocess.Popen(f"ping {ip_address} -t", shell=True, stdout=subprocess.DEVNULL)
processes.append(process)
thread = threading.Thread(target=check_ip_status, args=(ip_address, label))
thread.start()
root.after(5000, check_all_ip_statuses) 
gc.collect() 
# Close all subprocesses
for process in processes:
process.kill()
process.communicate()
check_all_ip_statuses()
root.mainloop()

I've tried to force the garbage collector, kill all processes and to clear the labels every so often but nothing stops tkinter from hoarding more and more ram

-------------Update---------

I've changed the subprocess call to pythonping

import pythonping
import time
import tkinter as tk
import threading
import gc
ip_addresses = ["192.168.1.83", "192.168.1.85", "192.168.1.93", "192.168.1.92", "192.168.1.95", "192.168.1.103", "192.168.1.105"]
i= 0
if i == 0:
root = tk.Tk()
root.title("Online")
root.geometry("500x300")
i+1
def create_labels():
global labels
labels = [tk.Label(root, text=f"{ip_address} -> unknown", font=("Arial", 16), fg="#FF0000") for ip_address in ip_addresses]
for label in labels:
label.pack(padx=10, pady=5)
def check_ip_status(ip_address, label):
response = pythonping.ping(ip_address, count=1, timeout=5)
if response.success():
label.config(text=f"{ip_address} -> online", fg="#00FF00") # Green
else:
label.config(text=f"{ip_address} -> offline", fg="#FF0000") # Red
def check_all_ip_statuses():
for i, ip_address in enumerate(ip_addresses):
label = labels[i]
thread = threading.Thread(target=check_ip_status, args=(ip_address, label))
thread.start()
gc.collect() 
root.after(1000, check_all_ip_statuses) 
create_labels()
check_all_ip_statuses()
root.mainloop()

答案1

得分: 1

我尝试找出你的版本为什么会占用内存,但我还没搞清楚。现在我找不到如何以实际方式使用所有内存分析工具来找出源代码中的问题。

所以,我创建了另一个版本的你的应用,我采用了面向对象的方法。

import sys
import threading
import time
import tkinter

import pythonping


class PingHost(threading.Thread):
    def __init__(self, ip, idx):
        super().__init__()
        self.daemon = True
        self.start_time = time.time()
        self.ip = ip
        self.idx = idx
        self.message = ''
        self.success = False

    def run(self) -> None:
        result = pythonping.ping(self.ip, count=1, timeout=5)
        if result.success():
            self.message = f'{self.ip} -> online'
            self.success = True
        else:
            self.message = f'{self.ip} -> offline'
            self.success = False


class App(tkinter.Frame):
    IP_ADDRESSES = [
        '192.168.80.101', '192.168.80.111', '192.168.80.123', '192.168.80.124', '192.168.80.125', '192.168.80.126',
        '192.168.80.127', '192.168.80.128'
    ]
    COLOR_ONLINE = '#00FF00'
    COLOR_OFFLINE = '#FF0000'
    FONT = ('Arial', 16)
    PING_THROTTLE = 10  # delay until a ping thread restarts

    def __init__(self, tk_root: tkinter.Tk):
        super().__init__(tk_root)
        self.root = tk_root
        self.root.protocol('WM_DELETE_WINDOW', self._on_quit)

        self.labels: [tkinter.Label] = []
        self.procs: [threading.Thread] = []

        self._create_widgets()

        self._ping_all_ips()
        self._monitor_procs()

    def _create_widgets(self):
        for ip in self.IP_ADDRESSES:
            string = f'{ip} -> unknown'
            label = tkinter.Label(self.root, text=string, fg=self.COLOR_OFFLINE, font=self.FONT)
            label.pack()
            self.labels.append(label)

    def _ping_all_ips(self):
        for idx, ip in enumerate(self.IP_ADDRESSES):
            self._start_ping_proc(ip, idx)

    def _start_ping_proc(self, ip, idx):
        ping_proc = PingHost(ip, idx)
        ping_proc.start()
        self.procs.append(ping_proc)

    def _monitor_procs(self):
        proc: PingHost
        for proc in self.procs:
            age = time.time() - proc.start_time

            if proc.is_alive():
                continue

            if age < self.PING_THROTTLE:
                self._update_labels(proc)
                continue

            self._update_labels(proc)
            self.procs.remove(proc)
            self._start_ping_proc(proc.ip, proc.idx)
        self.root.after(1000, self._monitor_procs)

    def _update_labels(self, proc):
        if "online" in proc.message:
            self.labels[proc.idx].configure(text=proc.message, fg=self.COLOR_ONLINE)
        else:
            self.labels[proc.idx].configure(text=proc.message, fg=self.COLOR_OFFLINE)

    def _on_quit(self):
        self.root.quit()
        sys.exit()


if __name__ == '__main__':
    root = tkinter.Tk()
    root.title('oop threading test')
    root.geometry('400x400')
    app = App(root)
    root.mainloop()

昨晚整个夜晚都在运行,内存一直维持在约12MB左右。
我有很多理论可以解释为什么这个版本能运行而你的版本不行,但目前我无法证明其中任何一个理论。

如果有人能给予启示,将不胜感激。

如果我发现更多信息,我会更新。

英文:

I tried to find out why you version is filling up memory, but I'm not there yet.
Right now I couldn't find out how to use all the memory analyzing tools in a practical way to find the culprit in your source.

So, I created another version of your app, the way I would do it using an OOP approach.

import sys
import threading
import time
import tkinter
import pythonping
class PingHost(threading.Thread):
def __init__(self, ip, idx):
super().__init__()
self.daemon = True
self.start_time = time.time()
self.ip = ip
self.idx = idx
self.message = &#39;&#39;
self.success = False
def run(self) -&gt; None:
result = pythonping.ping(self.ip, count=1, timeout=5)
if result.success():
self.message = f&#39;{self.ip} -&gt; online&#39;
self.success = True
else:
self.message = f&#39;{self.ip} -&gt; offline&#39;
self.success = False
class App(tkinter.Frame):
IP_ADDRESSES = [
&#39;192.168.80.101&#39;, &#39;192.168.80.111&#39;, &#39;192.168.80.123&#39;, &#39;192.168.80.124&#39;, &#39;192.168.80.125&#39;, &#39;192.168.80.126&#39;,
&#39;192.168.80.127&#39;, &#39;192.168.80.128&#39;
]
COLOR_ONLINE = &#39;#00FF00&#39;
COLOR_OFFLINE = &#39;#FF0000&#39;
FONT = (&#39;Arial&#39;, 16)
PING_THROTTLE = 10  # delay until a ping thread restarts
def __init__(self, tk_root: tkinter.Tk):
super().__init__(tk_root)
self.root = tk_root
self.root.protocol(&#39;WM_DELETE_WINDOW&#39;, self._on_quit)
self.labels: [tkinter.Label] = []
self.procs: [threading.Thread] = []
self._create_widgets()
self._ping_all_ips()
self._monitor_procs()
def _create_widgets(self):
for ip in self.IP_ADDRESSES:
string = f&#39;{ip} -&gt; unknown&#39;
label = tkinter.Label(self.root, text=string, fg=self.COLOR_OFFLINE, font=self.FONT)
label.pack()
self.labels.append(label)
def _ping_all_ips(self):
for idx, ip in enumerate(self.IP_ADDRESSES):
self._start_ping_proc(ip, idx)
def _start_ping_proc(self, ip, idx):
ping_proc = PingHost(ip, idx)
ping_proc.start()
self.procs.append(ping_proc)
def _monitor_procs(self):
proc: PingHost
for proc in self.procs:
age = time.time() - proc.start_time
if proc.is_alive():
continue
if age &lt; self.PING_THROTTLE:
self._update_labels(proc)
continue
self._update_labels(proc)
self.procs.remove(proc)
self._start_ping_proc(proc.ip, proc.idx)
self.root.after(1000, self._monitor_procs)
def _update_labels(self, proc):
if &quot;online&quot; in proc.message:
self.labels[proc.idx].configure(text=proc.message, fg=self.COLOR_ONLINE)
else:
self.labels[proc.idx].configure(text=proc.message, fg=self.COLOR_OFFLINE)
def _on_quit(self):
self.root.quit()
sys.exit()
if __name__ == &#39;__main__&#39;:
root = tkinter.Tk()
root.title(&#39;oop threading test&#39;)
root.geometry(&#39;400x400&#39;)
app = App(root)
root.mainloop()

This was running the whole night yesterday, with memory constantly around 12MB
I've got a lot of theories why this is working and yours is not, but I can't proove any of it atm.

So if anyone can shed light, it would be highly appreciated.

I'll update if I find more.

答案2

得分: 0

以下是您要求的代码部分的中文翻译:

这个版本是您的代码带有一些更新

我认为我终于理解了您添加的部分

i = 0
if i == 0:
root = tk.Tk()
root.title("Online")
root.geometry("500x300")
i+1


这是不必要的,因为您的主应用程序不会重复执行。`root.mainloop()`实际上是一个循环,但不会一遍又一遍地重新启动您的应用程序,因此可以删除它。
至于您关于`if __name__ == '__main__':`的注释可以修复它,实际上不会。这是为了防止您的脚本在被导入时执行。

print('如果被导入,我将打印')

if name == 'main':
print('如果被导入,这将不会打印,但如果作为脚本执行,则会打印')


我创建了一个扩展了`threading.Thread`的`Ping`类,以便我可以向线程添加属性,然后运行它。
与您的代码最大的不同之处在于,我在`check_all_ip_statuses`函数中检查所有正在运行的线程,如果线程未完成,我将继续执行。如果线程已完成,我将读取`response`属性以更新标签。
这样标签不是从线程内部更新的,似乎可以解决内存泄漏问题。
```python
from typing import List
import pythonping
import tkinter as tk
import threading
class Ping(threading.Thread):
def __init__(self, ip_address, idx):
super().__init__()
self.daemon = True
self.ip_address = ip_address
self.idx = idx
self.response: bool = False
def run(self):
response = pythonping.ping(self.ip_address, count=1, timeout=5)
self.response = response.success()
def create_labels():
global labels
labels = [tk.Label(root, text=f"{ip_address} -> 未知", font=("Arial", 16), fg="#FF0000") for ip_address in ip_addresses]
for label in labels:
label.pack(padx=10, pady=5)
def start_threads():
for i, ip_address in enumerate(ip_addresses):
thread = Ping(ip_address, i)
thread.start()
threads.append(thread)
def check_all_ip_statuses():
for thread in threads:
if thread.is_alive():
continue
if thread.response:
labels[thread.idx].config(text=f"{thread.ip_address} -> 在线", fg="#00FF00")  # 绿色
else:
labels[thread.idx].config(text=f"{thread.ip_address} -> 离线", fg="#FF0000")  # 红色
new_thread = Ping(thread.ip_address, thread.idx)
new_thread.start()
threads.remove(thread)
threads.append(new_thread)
root.after(1000, check_all_ip_statuses)
ip_addresses = ["192.168.1.83", "192.168.1.85", "192.168.1.93", "192.168.1.92", "192.168.1.95", "192.168.1.103",
"192.168.1.105"]
labels: List[tk.Label]
threads: List[Ping] = []
root = tk.Tk()
root.title("Online")
root.geometry("500x300")
create_labels()
start_threads()
check_all_ip_statuses()
root.mainloop()

正如@ShadowRanger在您的问题评论中提到的,关于是否可以从线程中更新标签:

@TimRoberts: 嗯,有时候可以。然后有时候一切都会崩溃。你不应该这样做,但人们有时会变懒,有时会奏效,直到它不奏效。

看起来他是对的。

英文:

So this version is your code, with some updates.

I think I finally understand this part you added:

i= 0
if i == 0:
root = tk.Tk()
root.title(&quot;Online&quot;)
root.geometry(&quot;500x300&quot;)
i+1

This is not needed, as your main application does not repeat itself. The root.mainloop() is in fact a loop in itself, but does not restart your application over and over again so you can remove it.

As for your comment about if __name__ == &#39;__main__&#39;: would fix it, not it would not. This is meant to prevent your script from executing if it is imported.

print(&#39;I will print if this gets imported&#39;)
if __name__ == &#39;__main__&#39;:
print(&#39;this will not print, if this gets imported, but will if it is executed as a script&#39;)

I created a Ping class that extends threading.Thread so I can add attributes to the thread and then run it.

So the main thing that is different form your code, is that I check all running threads in the check_all_ip_statuses function and just continue if the thread is not finished. If it is finished, I'll read out the response attribute to update the lables.

This way the labels are not updated from inside the thread and this seems to fix the memory leak problem.

from typing import List
import pythonping
import tkinter as tk
import threading
class Ping(threading.Thread):
def __init__(self, ip_address, idx):
super().__init__()
self.daemon = True
self.ip_address = ip_address
self.idx = idx
self.response: bool = False
def run(self):
response = pythonping.ping(self.ip_address, count=1, timeout=5)
self.response = response.success()
def create_labels():
global labels
labels = [tk.Label(root, text=f&quot;{ip_address} -&gt; unknown&quot;, font=(&quot;Arial&quot;, 16), fg=&quot;#FF0000&quot;) for ip_address in
ip_addresses]
for label in labels:
label.pack(padx=10, pady=5)
def start_threads():
for i, ip_address in enumerate(ip_addresses):
thread = Ping(ip_address, i)
thread.start()
threads.append(thread)
def check_all_ip_statuses():
for thread in threads:
if thread.is_alive():
continue
if thread.response:
labels[thread.idx].config(text=f&quot;{thread.ip_address} -&gt; online&quot;, fg=&quot;#00FF00&quot;)  # Green
else:
labels[thread.idx].config(text=f&quot;{thread.ip_address} -&gt; offline&quot;, fg=&quot;#FF0000&quot;)  # Red
new_thread = Ping(thread.ip_address, thread.idx)
new_thread.start()
threads.remove(thread)
threads.append(new_thread)
root.after(1000, check_all_ip_statuses)
ip_addresses = [&quot;192.168.1.83&quot;, &quot;192.168.1.85&quot;, &quot;192.168.1.93&quot;, &quot;192.168.1.92&quot;, &quot;192.168.1.95&quot;, &quot;192.168.1.103&quot;,
&quot;192.168.1.105&quot;]
labels: List[tk.Label]
threads: List[Ping] = []
root = tk.Tk()
root.title(&quot;Online&quot;)
root.geometry(&quot;500x300&quot;)
create_labels()
start_threads()
check_all_ip_statuses()
root.mainloop()

As @ShadowRanger mentioned in one of the comments of your question, about if it is okay to update labels from a thread:

@TimRoberts: Well, you can. Sometimes. And then sometimes everything breaks. You shouldn&#39;t do it, but people get lazy and sometimes it works, right up until it doesn&#39;t.

Seems like he is right.

huangapple
  • 本文由 发表于 2023年4月4日 05:21:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/75923865.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定