Let's say that you want to start a thread and run some background processing there but also you want to stop the program when you press CTRL+C or set a time out. (if you want to close all threads scroll down to the end)
We have 2 options:
- Just close the thread and don't care what happens
- Wait for the background thread to finish the current task and then return from it
Note
Daemon threads are abruptly stopped at shutdown. Their resources (such as open files, database transactions, etc.) may not be released properly. If you want your threads to stop gracefully, make them non-daemonic and use a suitable signalling mechanism such as an
Event
. (from python documentation)
If we manage to close the main-thread all the daemon threads will be forced to close.
Using daemon threads for tasks that can be interrupted
1. Using try/except for KeyboardInterrupt
#!/usr/bin/env python3 import sys import threading from time import sleep def process(): """Long lasting operation""" sleep(300) print("finished work") def main(argv): t = threading.Thread(target=process) t.daemon = True t.start() try: while t.is_alive(): print("Waiting for background thread to finish") sleep(1) print("Thread finished task, exiting") except KeyboardInterrupt: print("Closing main-thread.This will also close the background thread because is set as daemon.") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))
2. Using signal.signal to exit main thread
#!/usr/bin/env python3 import signal import sys import threading from time import sleep def process(): """Long lasting operation""" sleep(300) print("finished work") def signal_handler(signal, frame): print("Closing main-thread.This will also close the background thread because is set as daemon.") sys.exit(0) def main(argv): signal.signal(signal.SIGINT, signal_handler) t = threading.Thread(target=process) t.daemon = True t.start() while t.is_alive(): print("Waiting for background thread to finish") sleep(1) print("Thread finished task, exiting") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))
3. Using join() to add a timeout and then close all threads (can't catch CTRL+C)
#!/usr/bin/env python3 import sys import threading from time import sleep TIMEOUT = 10 def process(): """Long lasting operation""" sleep(12) print("finished work") def main(argv): """ This example has timeout using the join() method. The join() is blocking, meaning that an SIGINT event(CTRL+C) can't be detected. :param argv: :return: """ t = threading.Thread(target=process) t.daemon = True t.start() print(f"Waiting {TIMEOUT} seconds for background thread to finish.") t.join(TIMEOUT) if t.is_alive(): print("Background thread timed out. Closing all threads") return -1 else: print("Background thread finished processing. Closing all threads") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))
4. Using is_alive() for timeout and catching CTRL+C
#!/usr/bin/env python3 import sys import threading from time import sleep TIMEOUT = 10 def process(): """Long lasting operation""" sleep(12) print("finished work") def main(argv): """ This example combines timeout and KeyboardInterrupt. If detected KeyboardInterrupt all threads are closed. The background thread is close automatically because is set as daemon. :param argv: :return: """ t = threading.Thread(target=process) t.daemon = True t.start() print(f"Waiting {TIMEOUT} seconds for background thread to finish.") try: loop_thread(t) except KeyboardInterrupt: print("KeyboardInterrupt detected closing all threads") def loop_thread(t): i = 1 while i <= TIMEOUT: if t.is_alive(): print("Background thread processing, please wait.") sleep(1) i += 1 else: print("Background thread finished processing. Closing all threads") if i == TIMEOUT + 1: print("Timeout occurred.") if __name__ == "__main__": sys.exit(main(sys.argv))
Closing an non daemon thread gracefully (waiting for thread to finish work)
1. Using Event() to stop the thread gracefully
#!/usr/bin/env python3 import queue import sys import threading from time import sleep stop = threading.Event() work_queue = queue.Queue() work_queue.put('Item 1') work_queue.put('Item 2') def process(): """Long lasting operation""" while not stop.isSet() and not work_queue.empty(): sleep(5) item = work_queue.get() # process item print(f"Done task: {item}") work_queue.task_done() if stop.isSet(): print("KeyboardInterrupt detected, closing background thread. ") def main(argv): t = threading.Thread(target=process) t.start() try: while t.is_alive(): print("Waiting for background thread to finish") sleep(1) except KeyboardInterrupt: stop.set() print("Closing main-thread.Please wait for background thread to finish the current item.") return 0 work_queue.join() print("Thread finished all tasks, exiting") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))
Forcefully closing all threads os._exit(), even those that are not a daemon thread
#!/usr/bin/env python3 import os import queue import sys import threading from time import sleep work_queue = queue.Queue() work_queue.put('Item 1') work_queue.put('Item 2') def process(): """Long lasting operation""" while not work_queue.empty(): sleep(5) item = work_queue.get() # process item print(f"Done task: {item}") work_queue.task_done() def main(argv): t = threading.Thread(target=process) t.start() try: while t.is_alive(): print("Waiting for background thread to finish") sleep(1) except KeyboardInterrupt: print("Closing main-thread.Please wait for background thread to finish the current item.") if threading.activeCount() > 1: os._exit(getattr(os, "_exitcode", 0)) else: sys.exit(getattr(os, "_exitcode", 0)) work_queue.join() print("Thread finished all tasks, exiting") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))
You could also look into concurrent.futures depending on the situation the code could be easier to write using concurrent.futures.
Consider use of timeout with threads all the time in production, if code shouldn't take more than 10 seconds use timeout, is always better to see an error in a log file that threaded timed out than having the issue hidden for probably hours and then you start debugging, there is no need to lose time and money.
When to use threads? When you wait on I/O. If you do cpu intensive work look into subprocess module
If you want to learn more about threads and concurrency in python you can watch this youtube video by python core developer Raymond Hettinger.
Comments
Post a Comment