0

I wrote a small example of a program that is supposed to start running multiple batches of parallel threads after a start_button is pressed. This procedure can be launched just once, and after one batch of threads finishes, it is supposed to check if it is time to exit.

So for example, it can go like:
- Batch 1 (10 threads) is running, during it stop_button is pressed. After batch 1 is done, program should stop without running batch 2 and return to initial state (again having the option to launch this procedure).


But, GUI doesn't seem to be able to register a click or anything at all during this procedure. It just seems frozen. So I'm supposed to somehow separate threads doing their thing from GUI doing its thing, but I don't know how exactly.

import threading
import tkinter as tk
import time
import random


class Blocking():
    def __init__(self):
        self.master = tk.Tk()
        self.master.geometry("400x400")

        self.start_button = tk.Button(self.master, command=self.long_task, text='press me to start', state='normal')
        self.start_button.pack()

        self.stop_button = tk.Button(self.master, command=self.stop_func, text='press me to stop', state='normal')
        self.stop_button.pack()
        self.long_task_was_stopped = False

        self.master.mainloop()

    def one_thread(self, thread_index):
        time.sleep(random.randint(5, 10))

    def long_task(self): # will run many batches of parallel one_thread functions on press of start_button
        self.start_button["state"] = 'disabled'
        # first batch of threads
        threads = []
        for thread_number in range(0,10):
            thread = threading.Thread(target=self.one_thread, args=(thread_number,))
            threads.append(thread)
            thread.start()

        for thread in threads:
            thread.join()

        print("First batch over!")
        # batch over, check if it was stopped
        print("Stop variable value:", self.long_task_was_stopped)
        if self.long_task_was_stopped == True:
            # reset states, quit function
            self.long_task_was_stopped = False
            self.start_button["state"] = 'normal'
            print("Stopped, exiting!")
            return

        # second batch of threads
        threads = []
        for thread_number in range(0,10):
            thread = threading.Thread(target=self.one_thread, args=(thread_number,))
            threads.append(thread)
            thread.start()

        for thread in threads:
            thread.join()

        print("Second batch over!")
        self.long_task_was_stopped = False
        self.start_button["state"] = 'normal'
        print("Done.")
        return

    def stop_func(self):
        print("Trying to stop...")
        self.long_task_was_stopped = True



if __name__ == '__main__':
    block = Blocking()  



EDIT: It seems the solution is to keep calling update() on main Tkinter window after threads are started and check until all threads are over before proceeding, some kind of counter and threading.Lock() are needed for this. Here is the solution.

import threading
import tkinter as tk
import time
import random


class Blocking():
    def __init__(self):
        self.master = tk.Tk()
        self.master.geometry("400x400")

        self.start_button = tk.Button(self.master, command=self.long_task, text='press me to start', state='normal')
        self.start_button.pack()

        self.stop_button = tk.Button(self.master, command=self.stop_func, text='press me to stop', state='normal')
        self.stop_button.pack()
        self.long_task_was_stopped = False

        self.LOCK = threading.Lock()
        self.count_of_done_threads = 0
        self.master.mainloop()


    def one_thread(self, thread_index):
        time.sleep(random.randint(5, 10))
        with self.LOCK:
            print("Thread", thread_index, "done.")
            self.count_of_done_threads = self.count_of_done_threads +1

    def long_task(self): # will run many batches of parallel one_thread functions on press of start_button
        self.start_button["state"] = 'disabled'
        self.long_task_was_stopped = False

        # first batch of threads
        threads = []
        for thread_number in range(0,10):
            thread = threading.Thread(target=self.one_thread, args=(thread_number,))
            threads.append(thread)
            thread.start()

        # wait until threads are done
        while 1:
            self.master.update()
            if self.count_of_done_threads == 10: # 10 here is size of batch
                break
        self.count_of_done_threads = 0

        print("First batch over!")
        # batch over, check if it was stopped
        print("Stop variable value:", self.long_task_was_stopped)
        if self.long_task_was_stopped == True:
            # reset states, quit function
            self.long_task_was_stopped = False
            self.start_button["state"] = 'normal'
            print("Stopped, exiting!")
            return

        # second batch of threads
        threads = []
        for thread_number in range(0,10):
            thread = threading.Thread(target=self.one_thread, args=(thread_number,))
            threads.append(thread)
            thread.start()

        # wait until threads are done
        while 1:
            self.master.update()
            if self.count_of_done_threads == 10:
                break
        self.count_of_done_threads = 0

        print("Second batch over!")
        self.long_task_was_stopped = False
        self.start_button["state"] = 'normal'
        print("Done.")
        return

    def stop_func(self):
        print("Trying to stop...")
        self.long_task_was_stopped = True

if __name__ == '__main__':
    block = Blocking()  
DoctorEvil
  • 453
  • 3
  • 6
  • 18
  • The usage of `thread.join()` and `while 1:` makes the usage of `Thread ` to prevent the `tkinter.mainloop()` from freezing worthless. Implement to generate a event, after all `Thread` have finished and then start the next batch. See for reference: [`.event_generate(...`](https://stackoverflow.com/questions/60670374/progressbar-finishes-before-set-maximum-amount/60685778) – stovfl Mar 28 '20 at 17:53
  • 2nd option does seem to work though. Looks like calling update method on main window keeps mainloop alive after starting threads. And I'm not using thread.join() and while 1: together if that is what you mean. – DoctorEvil Mar 28 '20 at 18:43
  • ***"calling `update` method"***: Feel free to do so, but it's only a workaround. ***" together"***: That's not the point, both are blocking and have to be avoided. Read, understand [Event-driven programming](https://stackoverflow.com/a/9343402/7414759) – stovfl Mar 28 '20 at 19:35
  • I don't really understand, how else can you make sure threads are finished with their work if not by either joining or counting finished threads in a while loop? Or there is supposed to be a mechanism to join the threads like usual, but in some event handler that wouldn't block GUI? – DoctorEvil Mar 28 '20 at 21:27
  • ***"how else can you make sure threads are finished"***: See, the answer in the given link [`.event_generate(...`](https://stackoverflow.com/questions/60670374/progressbar-finishes-before-set-maximum-amount/60685778) – stovfl Mar 28 '20 at 22:15

1 Answers1

0

You should use non-block thread.Just thread.start() is Okay. In official document:

Other threads can call a thread’s join() method. This blocks the calling thread until the thread whose join() method is called is terminated.

This means that only when your function one_thread finish its work,your code will be run.

You code maybe should be :

import threading
import tkinter as tk
import time
import random


class Blocking():
    def __init__(self):
        self.master = tk.Tk()
        self.master.geometry("400x400")

        self.start_button = tk.Button(self.master, command=self.long_task, text='press me to start', state='normal')
        self.start_button.pack()

        self.stop_button = tk.Button(self.master, command=self.stop_func, text='press me to stop', state='normal')
        self.stop_button.pack()
        self.long_task_was_stopped = False

        self.master.mainloop()

    def one_thread(self, thread_index):
        time.sleep(random.randint(5, 10))

    def long_task(self): # will run many batches of parallel one_thread functions on press of start_button
        self.start_button["state"] = 'disabled'
        # first batch of threads
        threads = []
        for thread_number in range(0,10):
            thread = threading.Thread(target=self.one_thread, args=(thread_number,))
            threads.append(thread)
            thread.start()

        print("First batch over!")
        # batch over, check if it was stopped
        print("Stop variable value:", self.long_task_was_stopped)
        if self.long_task_was_stopped == True:
            # reset states, quit function
            self.long_task_was_stopped = False
            self.start_button["state"] = 'normal'
            print("Stopped, exiting!")
            return

        # second batch of threads
        threads = []
        for thread_number in range(0,10):
            thread = threading.Thread(target=self.one_thread, args=(thread_number,))
            threads.append(thread)
            thread.start()

        print("Second batch over!")
        self.long_task_was_stopped = False
        self.start_button["state"] = 'normal'
        print("Done.")
        return

    def stop_func(self):
        print("Trying to stop...")
        self.long_task_was_stopped = True

if __name__ == '__main__':
    block = Blocking()  

My minimal example to scrape a web picture.(When you run this code, it will show loading... when finish scraping,it will show the picture).

import tkinter
from PIL import ImageTk,Image
import threading,requests
from io import BytesIO

def scrapeImage():
    image = ImageTk.PhotoImage(Image.open(BytesIO(requests.get('https://s2.ax1x.com/2020/02/08/1Wl4mT.md.jpg').content)).resize((200,200))) # crawl my image
    imagePos.image = image # keep reference.
    imagePos['image'] = image


root = tkinter.Tk()
thread = threading.Thread(target=scrapeImage)
thread.start()

imagePos = tkinter.Label(text="loading.....")
imagePos.grid()

root.mainloop()
jizhihaoSAMA
  • 12,336
  • 9
  • 27
  • 49
  • Thank you for your reply. I can't leave my code exactly like that because this way threads would keep launching and launching, I need to wait until a batch is done to launch another one since it is a script for web scraping. It seems that it works by calling update() on main Tkinter window all the time after launching a batch of threads and waiting until all threads are finished before moving on. – DoctorEvil Mar 28 '20 at 16:46
  • @DoctorEvil Yes,You need to create a `non-block` thread,and when it finish web scraping,you want to handle those datas,right?you can directly put these operations in your `one_thread` function.I update a minimal example to scrap a website image. – jizhihaoSAMA Mar 29 '20 at 04:26
  • I don't understand what is a `non-block` thread. When I search for that on the link you gave, it just appears under info about locks. I am running threads independent from each other, but I need to wait until one batch of threads finishes (which is usually done with `join`). This code with picture and GUI isn't really what my problem is about, that is just one thread that goes unchecked (you aren't using `join` on it) – DoctorEvil Mar 29 '20 at 14:14
  • @DoctorEvil `.join()` is the question,I think you should know what is block.`block` will prevent you run another code(in your code,`sleep()` means that after sleep some time,it will run your next code,that's why your GUI will be freezing.),In this time,if you use only `start()`, the another thread will sleep(or another work).And the main thread won't. – jizhihaoSAMA Mar 29 '20 at 14:38
  • I'm trying to run batches of threads, so for example run batch 1 (10 threads), **wait until these 10 are done without freezing GUI** and then check if button was pressed, and run another batch if it wasn't. If you don't use join, then you will start 10 threads in batch 1, 10 threads in batch 2 and so on... This wouldn't be good for a data scraping script, it would definitely hit some limits. – DoctorEvil Mar 29 '20 at 15:31
  • @DoctorEvil **If you don't use join, then you will start 10 threads in batch 1, 10 threads in batch 2 and so on..**.But in your first example of the code.You also start 10 threads in batch 1, 10 threads in batch 2 and so on...Right?I told you use `.start()` couldn't block your code.That's what I mean.Now you solved your problem.Like I said,you also didn't use `.join()` in your code.Now I actually know what you want to do. – jizhihaoSAMA Mar 29 '20 at 17:37