How to stop Thread via button?

Question:

I have a button that is supposed to stop a thread that is running a server function on another Python file. The solutions I’ve tried are as follows:

Solution #1: Threading.Event

mainmenu.py

import server as serv #as in server.py
import socket
from threading import Thread
import customtkinter as cust


class GUI2(cust.CTk): #second window; not the root
    def __init__(self):
        self.master2 = cust.CTkToplevel()
        self.master2.title("Admin/Host Lobby")
        self.master2.geometry(f"{906}x{400}")
        self.master2.protocol("WM_DELETE_WINDOW", self.leavewindow)
        self.leave = cust.CTkButton(self.master2, text = "Leave", fg_color = "Red", text_color = "White", hover_color = "Maroon", command = lambda: self.leavewindow())
        self.leave.grid(row = 0, column = 0, sticky = "n", padx = 1, pady = 1)

        self.thread = Thread(target = serv.startChat)
        self.thread.start()
        
    def leavewindow(self):
        serv.terminate() #calls function from server.py that sets flag to true
        self.thread.join() #wait for thread to close
        print("Thread closed")
        serv.commence() #calls function from server.py that sets flag to false in case window is accessed again without exiting the program
        self.master2.destroy()

server.py

import socket
import threading
import traceback

flag = threading.Event()

PORT = 5000
SERVER = socket.gethostbyname(socket.gethostname())
ADDRESS = (SERVER, PORT)
FORMAT = "utf-8"

clients, names = [], []
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(ADDRESS)


def terminate(): #set flag to true
    flag.set()

def commence(): #set flag to false
    flag.clear()

def startChat(): #function that listens for client connections; the aforementioned function I want to stop/break when button is pressed
    print("server is working on " + SERVER)
    server.listen(30)
    try:
        while not flag.is_set(): #function runs as long as flag is not true
            conn, addr = server.accept()
            name = conn.recv(1024).decode(FORMAT)
            clients.append(conn)
    except:
        print (traceback.format_exc())

Solution #2: Global Flag Variable

Same code as above but with the following changes:

mainmenu.py

def leavewindow(self):
    serv.setFlag(1) #passes integer 1 to set flag to True
    self.thread.join() #wait for thread to close
    print("Thread closed")
    serv.setFlag(0) #passes integer 0 to set flag to false in case window is accessed again without exiting the program
    self.master2.destroy()

server.py

flag = False
def setFlag(a): #function receives integer to set flag value
    global flag
    if a == 0:
        flag = False
    else:
        flag = True

def startChat(): #function that listens for client connections; the aforementioned function I want to stop/break when button is pressed
    print("server is working on " + SERVER)
    server.listen(30)
    try:
        while True:
            global flag
            if flag:
                break
            else:
                conn, addr = server.accept()
                name = conn.recv(1024).decode(FORMAT)
                clients.append(conn)
    except:
        print (traceback.format_exc())

Both of these "solutions" ends up freezing the window for an indefinite time; no errors printed in the terminal plus the line print("Thread closed") isn’t even reached.

Any advice or alternative solutions in fixing this bug is highly appreciated.

Asked By: N. Tanaka

||

Answers:

A (if not the) major cause of an unresponsive tkinter GUI is a callback that keeps running.

The only callback that we see is leavewindow().
In that function, basically only self.thread.join() can cause this.

By default, sockets are created in blocking mode.
So for example recv will wait until it receives data.
Which is not what you want, because your flag or event will not be tested until after data is received.

You could set a timeout on the conn socket so it raises an exception when no data is received.
In the handler for that exception, just sleep() in the while-loop for some milliseconds.

def startChat(): #function that listens for client connections; the aforementioned function I want to stop/break when button is pressed
    print("server is working on " + SERVER)
    server.listen(30)
     # Not sure if the following line is needed...
    server.settimeout(0.005) # time out after 5 ms.
    try:
        while not flag.is_set(): # works with global flag as well
            conn, addr = server.accept()
            conn.settimeout(0.005) # time out after 5 ms.
            name = conn.recv(1024).decode(FORMAT)
            clients.append(conn)
    except socket.timeout:
        time.sleep(0.005)
    except Exception:
        print (traceback.format_exc())

Or you could use the selectors module with a nonblocking socket to only try and read data if any is available. Here you should also sleep() in the while-loop for some milliseconds when no data is available.

Answered By: Roland Smith
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.