Using Threads and Signal makes unhang program
keywords: Threads; signal; hang; Python; Ctrl+C; SIGINT
一篇面向没有多线程,捕获信号方面编程经验 的 使用多线程以及捕获信号实现的 console/GUI 不hang 的简单 demo。
写这一篇 blog 的起因是一位同事问 GUI 上一个按钮点击之后,耗时太长, GUI hang 住。如何使用 多线程 使 GUI 不 hang。
实际上使用多线程使 GUI 不 hang 住还是相当容易实现的。
但是我个人非常能够感受到这种心理,就是大概知道怎么做,但是没有做过,就有一种去学多线程,捕获信号处理等等这种的障碍。
所以,我写了这篇 blog,重在示例,多线程和捕捉信号这类编程技术不算很难掌握,实际上只需要不到十行代码,已经适当调整一下代码结构(封装一些代码到一个函数中去)就能够使用了。
不过为了写这些一步一步的示例和录制 GIF 着实花了我很多时间。先发表上来,等什么时候有需要补充要说的细节了再来 update。
文章目录
Demo
SIGINT_Ctrl+C.py
Usage
$ chmod +x ./SIGINT_Ctrl+C.py
$
$ ./SIGINT_Ctrl+C.py
......
Ctrl+C
.....
$
Code
#!/usr/bin/env python3
"""https://github.com/RDpWTeHM/solution/tree/master/unhang/threads_and_signal/
SIGINT_Ctrl+C.py
Author: Joseph Lin
Email : joseph.lin@aliyun.com
Social:
https://github.com/RDpWTeHM
https://blog.youkuaiyun.com/qq_29757283
Note:
Ctrl+C <==> SIGINT
"""
import signal
from time import sleep
# Signal handler for termination (required)
def sigint_handler(signo, frame):
''' show catch Ctrl+C information!'''
print("\nGoodbay Cruel World.....\n")
raise SystemExit(1)
def prog_init():
signal.signal(signal.SIGINT, sigint_handler)
def main():
prog_init()
counter = 0
while True:
print("Guess what am I running....{}s".format(counter), end="")
sleep(1)
counter += 1
print("\r", end="")
if __name__ == '__main__':
main()
Test Result
SIGALRM_calculate-game.py
Usage
$ chmod +x ./SIGALRM_calculate-game.py
$
$ ./SIGALRM_calculate-game.py
[Debug] prog_pid <- os.getpid(): 28202
!!!!!game start!!!!!
Calculate 10 x 4 = ?: 40
Right!
Calculate 8 x 6 = ?: 48
Right!
Calculate 6 x 5 = ?:
Oooooops game time up!
Final score: 2
$
Code
#!/usr/bin/env python3
"""https://github.com/RDpWTeHM/solution/tree/master/unhang/threads_and_signal/
SIGALRM_calculate-game.py
Author: Joseph Lin
Email : joseph.lin@aliyun.com
Social:
https://github.com/RDpWTeHM
https://blog.youkuaiyun.com/qq_29757283
Note:
signal.alarm(integer) ==> SIGALRM
"""
import sys
import os
import signal
from random import randint
score = 0
prog_pid = os.getpid()
if __debug__:
print("[Debug] prog_pid <- os.getpid(): {}".format(prog_pid))
def sigalrm_handler(signo, frame):
''' game time up'''
global prog_pid
print("\nOooooops game time up!")
os.kill(prog_pid, signal.SIGINT)
def sigint_handler(signo, frame):
''' end game'''
global score
print("\nFinal score: {}\n".format(score))
raise SystemExit(0)
def prog_init():
signal.signal(signal.SIGALRM, sigalrm_handler)
signal.signal(signal.SIGINT, sigint_handler)
def main():
global score
global prog_pid
prog_init()
print("!!!!!game start!!!!!")
while True:
a = randint(0, 10)
b = randint(0, 10)
signal.alarm(5) # set game time
print("Calculate {} x {} = ?: ".format(a, b), end='')
try:
user_input = int(input())
except ValueError:
print("Error input! Expect numbers.", file=sys.stderr)
os.kill(prog_pid, signal.SIGINT)
if a * b == user_input:
print("Right!\n")
score += 1
else:
os.kill(prog_pid, signal.SIGINT)
if __name__ == '__main__':
main()
Test Result
unhang_console_by_Threads_SIGALRM.py
Usage
$ pyton -c "import requests"
## run "pip3 install --user requests" if needed
$
$ time ./unhang_console_by_Threads_SIGALRM.py
Test Result
-
一个 console 转圈圈的程序
Code#!/usr/bin/env python3 import signal from time import sleep def sigint_handler(signo, frame): ''' end program''' print("") raise SystemExit(0) def prog_init(): signal.signal(signal.SIGINT, sigint_handler) def main(): prog_init() i = 0 circle = ('|', '/', '-', '\\') while True: sleep(0.2) print("\rprocessing... {}".format(circle[i]), end='') i = 0 if i == 3 else i + 1 if __name__ == '__main__': main()
Running
-
normal coding = console hang example:
Code:#!/usr/bin/env python3 import os import signal from time import sleep request_pages_result = {} prog_pid = os.getpid() if __debug__: print("[Debug] prog_pid <- os.getpid(): {}".format(prog_pid)) def sigint_handler(signo, frame): ''' end program''' print("") raise SystemExit(0) def prog_init(): signal.signal(signal.SIGINT, sigint_handler) def request_pages(): global request_pages_result sites = ("https://www.baidu.com", "https://www.bing.com", "https://www.yahoo.com", "http://www.so.com") import requests try: for site in sites: r = requests.get(site) if r.status_code != 200: request_pages_result[site] = False else: request_pages_result[site] = True except Exception as e: print("[Error] request_pages(): ", e, file=sys.stderr) def main(): global request_pages_result global prog_pid prog_init() request_pages() print(request_pages_result) i = 0 circle = ('|', '/', '-', '\\') while True: sleep(0.2) print("\rprocessing... {}".format(circle[i]), end='') i = 0 if i == 3 else i + 1 if __name__ == '__main__': main()
Usage:
$ pyton -c "import requests" ## run "pip install --user requests" if needed $ $ time ./unhang_console_by_Threads_SIGALRM.py [Debug] prog_pid <- os.getpid(): 28334 {'https://www.baidu.com': True, 'https://www.bing.com': True, 'https://www.yahoo.com': True, 'http://www.so.com': True} processing... \^C real 0m17.423s user 0m0.268s sys 0m0.026s $
Running:
-
use Thread, but main-thread wait sub-thread finish, then show the result, still console hang.
$ git diff [...] --- a/unhang/threads_and_signal/unhang_console_by_Threads_SIGALRM.py +++ b/unhang/threads_and_signal/unhang_console_by_Threads_SIGALRM.py @@ -19,6 +19,7 @@ import os import signal from time import sleep +import threading request_pages_result = {} @@ -72,8 +73,11 @@ def main(): - request_pages() - print(request_pages_result) + t = threading.Thread(target=request_pages) + t.setDaemon(True) + t.start() + t.join() + print("request pages result: \n", request_pages_result) i = 0 circle = ('|', '/', '-', '\\') $
run
$ time ./unhang_console_by_Threads_SIGALRM.py
just look like the same. -
use threads, no
.join()
, catch “Ctrl+C” before quit program, show the result.so, if run time is short, the function will not finish (
t.setDaemon(True)
must require). we can see different result if “processing” time is short and long.
Code#!/usr/bin/env python3 import signal from time import sleep import threading request_pages_result = {} def sigint_handler(signo, frame): ''' end program''' global request_pages_result print("\nGoodbay Cruel World.....") print("before exit program, show you the result:\n", request_pages_result) raise SystemExit(0) def prog_init(): signal.signal(signal.SIGINT, sigint_handler) def request_pages(): global request_pages_result sites = ("https://www.baidu.com", "https://www.bing.com", "https://www.yahoo.com", "http://www.so.com") import requests try: for site in sites: r = requests.get(site) if r.status_code != 200: request_pages_result[site] = False else: request_pages_result[site] = True except Exception as e: print("[Error] request_pages(): ", e, file=sys.stderr) def main(): global request_pages_result prog_init() t = threading.Thread(target=request_pages) t.setDaemon(True) t.start() print("request pages result: \n", request_pages_result) i = 0 circle = ('|', '/', '-', '\\') while True: sleep(0.2) print("\rprocessing... {}".format(circle[i]), end='') i = 0 if i == 3 else i + 1 if __name__ == '__main__': main()
Running:
-
use threads with signal, unhang console.
Code:#!/usr/bin/env python3 """unhang_console_by_Threads_SIGALRM.py Author: Joseph Lin Email : joseph.lin@aliyun.com Social: https://github.com/RDpWTeHM https://blog.youkuaiyun.com/qq_29757283 Note: signal.alarm(integer) ==> SIGALRM alarm may can't not work well with sleep in some python version. """ import os import signal from time import sleep import threading request_pages_result = {} prog_pid = os.getpid() if __debug__: print("[Debug] prog_pid <- os.getpid(): {}".format(prog_pid)) def sigalrm_handler(signo, frame): ''' time up''' global request_pages_result global prog_pid print("\nOooooops time up!") print("[sigalrm_handler] request pages result: \n", request_pages_result) os.kill(prog_pid, signal.SIGINT) def sigint_handler(signo, frame): ''' end program''' global request_pages_result print("\nGoodbay Cruel World.....") print("[sigint_handler] before exit program, show you the result:\n", request_pages_result) raise SystemExit(0) def prog_init(): signal.signal(signal.SIGALRM, sigalrm_handler) signal.signal(signal.SIGINT, sigint_handler) def request_pages(): global request_pages_result sites = ("https://www.baidu.com", "https://www.bing.com", "https://www.yahoo.com", "http://www.so.com") import requests try: for site in sites: r = requests.get(site) if r.status_code != 200: request_pages_result[site] = False else: request_pages_result[site] = True except Exception as e: print("[Error] request_pages(): ", e, file=sys.stderr) def main(): global request_pages_result global prog_pid prog_init() t = threading.Thread(target=request_pages) t.setDaemon(True) t.start() print("request pages result: \n", request_pages_result) print("start the function which will cost lots of time.") # 5 second later, run the register function which will show the result. signal.alarm(5) # main thread keep doing things i = 0 circle = ('|', '/', '-', '\\') while True: sleep(0.2) print("\rmain thread processing... {}".format(circle[i]), end='') i = 0 if i == 3 else i + 1 if __name__ == '__main__': main()
Running:
unhang_GUI_by_Threads.py
Usage
$ python -c "import tkinter"
## "pip3 install --user tkinter" if needed
## "requests" is also needed in this example
$
$ python ./unhang_GUI_by_Threads.py
Test Result
-
basic GUI demo
click button, show message on GUI.
Code:#!/usr/bin/env python3 from tkinter import * import tkinter as tk import sys def quit(): print("I will quit!") sys.exit() def clean(): print("Press Clean button") entry.delete(0, tk.END) def show(): print("Press Show button") entry.delete(0, tk.END) entry.insert(0, "Button Clicked!") def show_request_pages_reslut(): global request_pages_result entry.delete(0, tk.END) entry.insert(0, "{!s}".format(request_pages_result)) root = Tk() win = Frame(root) win.pack(expand=YES, fill=BOTH) entry = Entry(win) entry.config(text = "not clicked") entry.pack(expand=YES, fill=BOTH) btnShow = Button(win) btnShow.config(text="Show", command=show) btnShow.pack(expand=YES, fill=BOTH) btnClean = Button(win) btnClean.config(text="Clean", command=clean) btnClean.pack(expand=YES, fill=BOTH) btnQuit = Button(win) btnQuit.config(text='Quit', command=quit) btnQuit.pack() root.title('unhang GUI.py') root.mainloop()
Running:
GitHub SHA: 4a08bf4dd92dcc5ca0f2be1a3e72cf5bb5c34948
-
GUI hang demo
Note: this run on Windows system, the phenomenon of hang can’t be able to reproduce on Ubuntu.
Code:#!/usr/bin/env python3 from tkinter import * import tkinter as tk import sys request_pages_result = {} def quit(): print("I will quit!") sys.exit() def clean(): print("Press Clean button") entry.delete(0, tk.END) def show(): print("Press Show button") entry.delete(0, tk.END) entry.insert(0, "Button Clicked!") global request_pages_result sites = ("https://www.baidu.com", "https://www.bing.com", "https://www.yahoo.com", "http://www.so.com") import requests try: for site in sites: r = requests.get(site) if r.status_code != 200: request_pages_result[site] = False else: request_pages_result[site] = True except Exception as e: print("[Error] request_pages(): ", e, file=sys.stderr) show_request_pages_reslut() def show_request_pages_reslut(): global request_pages_result entry.delete(0, tk.END) entry.insert(0, "{!s}".format(request_pages_result)) root = Tk() win = Frame(root) win.pack(expand=YES, fill=BOTH) entry = Entry(win) entry.config(text = "not clicked") entry.pack(expand=YES, fill=BOTH) btnShow = Button(win) btnShow.config(text="Show", command=show) btnShow.pack(expand=YES, fill=BOTH) btnClean = Button(win) btnClean.config(text="Clean", command=clean) btnClean.pack(expand=YES, fill=BOTH) btnQuit = Button(win) btnQuit.config(text='Quit', command=quit) btnQuit.pack() root.title('unhang GUI.py') root.mainloop()
Running:
Button running a time-consuming processing, GUI will hang on Windows system.
GitHub SHA: d07b97cd1226f23eb9e644288cb8eeae1a685d3e -
GUI unhang by threads demo
no need signal in this case, threads make GUI unhang
Code:#!/usr/bin/env python3 from tkinter import * import tkinter as tk import sys import threading request_pages_result = {} def quit(): print("I will quit!") sys.exit() def clean(): print("Press Clean button") entry.delete(0, tk.END) def show(): print("Press Show button") entry.delete(0, tk.END) entry.insert(0, "Processing request pages...") t = threading.Thread(target=request_pages) t.setDaemon(True) t.start() def request_pages(): global request_pages_result sites = ("https://www.baidu.com", "https://www.bing.com", "https://www.yahoo.com", "http://www.so.com") import requests try: for site in sites: r = requests.get(site) if r.status_code != 200: request_pages_result[site] = False else: request_pages_result[site] = True except Exception as e: print("[Error] request_pages(): ", e, file=sys.stderr) show_request_pages_reslut() def show_request_pages_reslut(): global request_pages_result entry.delete(0, tk.END) entry.insert(0, "{!s}".format(request_pages_result)) root = Tk() win = Frame(root) win.pack(expand=YES, fill=BOTH) entry = Entry(win) entry.config(text = "not clicked") entry.pack(expand=YES, fill=BOTH) btnShow = Button(win) btnShow.config(text="Show", command=show) btnShow.pack(expand=YES, fill=BOTH) btnClean = Button(win) btnClean.config(text="Clean", command=clean) btnClean.pack(expand=YES, fill=BOTH) btnQuit = Button(win) btnQuit.config(text='Quit', command=quit) btnQuit.pack() root.title('unhang GUI.py') root.mainloop()
Runing:
GitHub SHA: c2a9b0215f9ee5cd2d8e458255e5ae9f9447e16d
codes of demo above, using Git log can see the different version code.