背景
开工前我就觉得有什么不太对劲,感觉要背锅。这可不,上班第三天就捅锅了。
我们有个了不起的后台程序,可以动态加载模块,并以线程方式运行,通过这种形式实现插件的功能。而模块更新时候,后台程序自身不会退出,只会将模块对应的线程关闭、更新代码再启动,6 得不行。
于是乎我就写了个模块准备大展身手,结果忘记写退出函数了,导致每次更新模块都新创建一个线程,除非重启那个程序,否则那些线程就一直苟活着。
这可不行啊,得想个办法清理呀,要不然怕是要炸了。
那么怎么清理呢?我能想到的就是两步走:
- 找出需要清理的线程号 tid;
- 销毁它们;
找出线程id
和平时的故障排查相似,先通过 ps 命令看看目标进程的线程情况,因为已经是 setname 设置过线程名,所以正常来说应该是看到对应的线程的。 直接用下面代码来模拟这个线程:
python 版本的多线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#coding: utf8 import threading import os import time def tt(): info = threading.currentthread() while true: print 'pid: ' , os.getpid() print info.name, info.ident time.sleep( 3 ) t1 = threading.thread(target = tt) t1.setname( 'oooooppppp' ) t1.setdaemon(true) t1.start() t2 = threading.thread(target = tt) t2.setname( 'eeeeeeeee' ) t2.setdaemon(true) t2.start() t1.join() t2.join() |
输出:
root@10-46-33-56:~# python t.py
pid: 5613
oooooppppp 139693508122368
pid: 5613
eeeeeeeee 139693497632512
...
可以看到在 python 里面输出的线程名就是我们设置的那样,然而 ps 的结果却是令我怀疑人生:
root@10-46-33-56:~# ps -tp 5613
pid spid tty time cmd
5613 5613 pts/2 00:00:00 python
5613 5614 pts/2 00:00:00 python
5613 5615 pts/2 00:00:00 python
正常来说不该是这样呀,我有点迷了,难道我一直都是记错了?用别的语言版本的多线程来测试下:
c 版本的多线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
#include<stdio.h> #include<sys/syscall.h> #include<sys/prctl.h> #include<pthread.h> void * test(void * name) { pid_t pid, tid; pid = getpid(); tid = syscall(__nr_gettid); char * tname = (char * )name; / / 设置线程名字 prctl(pr_set_name, tname); while ( 1 ) { printf( "pid: %d, thread_id: %u, t_name: %s\n" , pid, tid, tname); sleep( 3 ); } } int main() { pthread_t t1, t2; void * ret; pthread_create(&t1, null, test, (void * ) "love_test_1" ); pthread_create(&t2, null, test, (void * ) "love_test_2" ); pthread_join(t1, &ret); pthread_join(t2, &ret); } |
输出:
root@10-46-33-56:~# gcc t.c -lpthread && ./a.out
pid: 5575, thread_id: 5577, t_name: love_test_2
pid: 5575, thread_id: 5576, t_name: love_test_1
pid: 5575, thread_id: 5577, t_name: love_test_2
pid: 5575, thread_id: 5576, t_name: love_test_1
...
用 ps 命令再次验证:
root@10-46-33-56:~# ps -tp 5575
pid spid tty time cmd
5575 5575 pts/2 00:00:00 a.out
5575 5576 pts/2 00:00:00 love_test_1
5575 5577 pts/2 00:00:00 love_test_2
这个才是正确嘛,线程名确实是可以通过 ps 看出来的嘛!
不过为啥 python 那个看不到呢?既然是通过 setname
设置线程名的,那就看看定义咯:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
[threading.py] class thread(_verbose): ... @property def name( self ): """a string used for identification purposes only. it has no semantics. multiple threads may be given the same name. the initial name is set by the constructor. """ assert self .__initialized, "thread.__init__() not called" return self .__name def setname( self , name): self .name = name ... |
看到这里其实只是在 thread
对象的属性设置了而已,并没有动到根本,那肯定就是看不到咯~
这样看起来,我们已经没办法通过 ps
或者 /proc/
这类手段在外部搜索 python 线程名了,所以我们只能在 python 内部来解决。
于是问题就变成了,怎样在 python 内部拿到所有正在运行的线程呢?
threading.enumerate
可以完美解决这个问题!why?
because 在下面这个函数的 doc 里面说得很清楚了,返回所有活跃的线程对象,不包括终止和未启动的。
1
2
3
4
5
6
7
8
9
10
11
12
|
[threading.py] def enumerate (): """return a list of all thread objects currently alive. the list includes daemonic threads, dummy thread objects created by current_thread(), and the main thread. it excludes terminated threads and threads that have not yet been started. """ with _active_limbo_lock: return _active.values() + _limbo.values() |
因为拿到的是 thread 的对象,所以我们通过这个能到该线程相关的信息!
请看完整代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
#coding: utf8 import threading import os import time def get_thread(): pid = os.getpid() while true: ts = threading. enumerate () print '------- running threads on pid: %d -------' % pid for t in ts: print t.name, t.ident print time.sleep( 1 ) def tt(): info = threading.currentthread() pid = os.getpid() while true: print 'pid: {}, tid: {}, tname: {}' . format (pid, info.name, info.ident) time.sleep( 3 ) return t1 = threading.thread(target = tt) t1.setname( 'thread-test1' ) t1.setdaemon(true) t1.start() t2 = threading.thread(target = tt) t2.setname( 'thread-test2' ) t2.setdaemon(true) t2.start() t3 = threading.thread(target = get_thread) t3.setname( 'checker' ) t3.setdaemon(true) t3.start() t1.join() t2.join() t3.join() |
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
root@ 10 - 46 - 33 - 56 :~ # python t_show.py pid: 6258 , tid: thread - test1, tname: 139907597162240 pid: 6258 , tid: thread - test2, tname: 139907586672384 - - - - - - - running threads on pid: 6258 - - - - - - - mainthread 139907616806656 thread - test1 139907597162240 checker 139907576182528 thread - test2 139907586672384 - - - - - - - running threads on pid: 6258 - - - - - - - mainthread 139907616806656 thread - test1 139907597162240 checker 139907576182528 thread - test2 139907586672384 - - - - - - - running threads on pid: 6258 - - - - - - - mainthread 139907616806656 thread - test1 139907597162240 checker 139907576182528 thread - test2 139907586672384 - - - - - - - running threads on pid: 6258 - - - - - - - mainthread 139907616806656 checker 139907576182528 ... |
代码看起来有点长,但是逻辑相当简单,thread-test1
和 thread-test2
都是打印出当前的 pid、线程 id 和 线程名字,然后 3s 后退出,这个是想模拟线程正常退出。
而 checker
线程则是每秒通过 threading.enumerate
输出当前进程内所有活跃的线程。
可以明显看到一开始是可以看到 thread-test1
和 thread-test2
的信息,当它俩退出之后就只剩下 mainthread
和 checker
自身而已了。
销毁指定线程
既然能拿到名字和线程 id,那我们也就能干掉指定的线程了!
假设现在 thread-test2
已经黑化,发疯了,我们需要制止它,那我们就可以通过这种方式解决了:
在上面的代码基础上,增加和补上下列代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
def _async_raise(tid, exctype): """raises the exception, performs cleanup if needed""" tid = ctypes.c_long(tid) if not inspect.isclass(exctype): exctype = type (exctype) res = ctypes.pythonapi.pythreadstate_setasyncexc(tid, ctypes.py_object(exctype)) if res = = 0 : raise valueerror( "invalid thread id" ) elif res ! = 1 : ctypes.pythonapi.pythreadstate_setasyncexc(tid, none) raise systemerror( "pythreadstate_setasyncexc failed" ) def stop_thread(thread): _async_raise(thread.ident, systemexit) def get_thread(): pid = os.getpid() while true: ts = threading. enumerate () print '------- running threads on pid: %d -------' % pid for t in ts: print t.name, t.ident, t.is_alive() if t.name = = 'thread-test2' : print 'i am go dying! please take care of yourself and drink more hot water!' stop_thread(t) print time.sleep( 1 ) |
输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
root@ 10 - 46 - 33 - 56 :~ # python t_show.py pid: 6362 , tid: 139901682108160 , tname: thread - test1 pid: 6362 , tid: 139901671618304 , tname: thread - test2 - - - - - - - running threads on pid: 6362 - - - - - - - mainthread 139901706389248 true thread - test1 139901682108160 true checker 139901661128448 true thread - test2 139901671618304 true thread - test2: i am go dying. please take care of yourself and drink more hot water! - - - - - - - running threads on pid: 6362 - - - - - - - mainthread 139901706389248 true thread - test1 139901682108160 true checker 139901661128448 true thread - test2 139901671618304 true thread - test2: i am go dying. please take care of yourself and drink more hot water! pid: 6362 , tid: 139901682108160 , tname: thread - test1 - - - - - - - running threads on pid: 6362 - - - - - - - mainthread 139901706389248 true thread - test1 139901682108160 true checker 139901661128448 true / / thread - test2 已经不在了 |
一顿操作下来,虽然我们这样对待 thread-test2
,但它还是关心着我们:多喝热水,
ps: 热水虽好,八杯足矣,请勿贪杯哦。
书回正传,上述的方法是极为粗暴的,为什么这么说呢?
因为它的原理是:利用 python 内置的 api,触发指定线程的异常,让其可以自动退出;
为什么停止线程这么难
多线程本身设计就是在进程下的协作并发,是调度的最小单元,线程间分食着进程的资源,所以会有许多锁机制和状态控制。
如果使用强制手段干掉线程,那么很大几率出现意想不到的bug。 而且最重要的锁资源释放可能也会出现意想不到问题。
我们甚至也无法通过信号杀死进程那样直接杀线程,因为 kill 只有对付进程才能达到我们的预期,而对付线程明显不可以,不管杀哪个线程,整个进程都会退出!
而因为有 gil,使得很多童鞋都觉得 python 的线程是python 自行实现出来的,并非实际存在,python 应该可以直接销毁吧?
然而事实上 python 的线程都是货真价实的线程!
什么意思呢?python 的线程是操作系统通过 pthread 创建的原生线程。python 只是通过 gil 来约束这些线程,来决定什么时候开始调度,比方说运行了多少个指令就交出 gil,至于谁夺得花魁,得听操作系统的。
如果是单纯的线程,其实系统是有办法终止的,比如: pthread_exit
,pthread_kill
或 pthread_cancel
, 详情可看:
很可惜的是: python 层面并没有这些方法的封装!我的天,好气!可能人家觉得,线程就该温柔对待吧。
如何温柔退出线程
想要温柔退出线程,其实差不多就是一句废话了~
要么运行完退出,要么设置标志位,时常检查标记位,该退出的就退出咯。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://segmentfault.com/a/1190000018177256