Python线程之定位与销毁的实现


Posted in Python onFebruary 17, 2019

背景

开工前我就觉得有什么不太对劲,感觉要背锅。这可不,上班第三天就捅锅了。

我们有个了不起的后台程序,可以动态加载模块,并以线程方式运行,通过这种形式实现插件的功能。而模块更新时候,后台程序自身不会退出,只会将模块对应的线程关闭、更新代码再启动,6 得不行。

于是乎我就写了个模块准备大展身手,结果忘记写退出函数了,导致每次更新模块都新创建一个线程,除非重启那个程序,否则那些线程就一直苟活着。

这可不行啊,得想个办法清理呀,要不然怕是要炸了。

那么怎么清理呢?我能想到的就是两步走:

  • 找出需要清理的线程号 tid;
  • 销毁它们;

找出线程ID

和平时的故障排查相似,先通过 ps 命令看看目标进程的线程情况,因为已经是 setName 设置过线程名,所以正常来说应该是看到对应的线程的。 直接用下面代码来模拟这个线程:

Python 版本的多线程

#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 版本的多线程

#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 设置线程名的,那就看看定义咯:

[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 里面说得很清楚了,返回所有活跃的线程对象,不包括终止和未启动的。

[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 的对象,所以我们通过这个能到该线程相关的信息!

请看完整代码示例:

#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()

输出:

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-test1Thread-test2 都是打印出当前的 pid、线程 id 和 线程名字,然后 3s 后退出,这个是想模拟线程正常退出。

Checker 线程则是每秒通过 threading.enumerate 输出当前进程内所有活跃的线程。

可以明显看到一开始是可以看到 Thread-test1Thread-test2的信息,当它俩退出之后就只剩下 MainThreadChecker 自身而已了。

销毁指定线程

既然能拿到名字和线程 id,那我们也就能干掉指定的线程了!

假设现在 Thread-test2 已经黑化,发疯了,我们需要制止它,那我们就可以通过这种方式解决了:

在上面的代码基础上,增加和补上下列代码:

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)

输出

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,触发指定线程的异常,让其可以自动退出;

Python线程之定位与销毁的实现

为什么停止线程这么难

多线程本身设计就是在进程下的协作并发,是调度的最小单元,线程间分食着进程的资源,所以会有许多锁机制和状态控制。

如果使用强制手段干掉线程,那么很大几率出现意想不到的bug。 而且最重要的锁资源释放可能也会出现意想不到问题。

我们甚至也无法通过信号杀死进程那样直接杀线程,因为 kill 只有对付进程才能达到我们的预期,而对付线程明显不可以,不管杀哪个线程,整个进程都会退出!

而因为有 GIL,使得很多童鞋都觉得 Python 的线程是Python 自行实现出来的,并非实际存在,Python 应该可以直接销毁吧?

然而事实上 Python 的线程都是货真价实的线程!

什么意思呢?Python 的线程是操作系统通过 pthread 创建的原生线程。Python 只是通过 GIL 来约束这些线程,来决定什么时候开始调度,比方说运行了多少个指令就交出 GIL,至于谁夺得花魁,得听操作系统的。

如果是单纯的线程,其实系统是有办法终止的,比如: pthread_exit,pthread_killpthread_cancel, 详情可看:https://3water.com/article/156412.htm

很可惜的是: Python 层面并没有这些方法的封装!我的天,好气!可能人家觉得,线程就该温柔对待吧。

如何温柔退出线程

想要温柔退出线程,其实差不多就是一句废话了~

要么运行完退出,要么设置标志位,时常检查标记位,该退出的就退出咯。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python写的Discuz7.2版faq.php注入漏洞工具
Aug 06 Python
python迭代器与生成器详解
Mar 10 Python
python使用turtle库绘制时钟
Mar 25 Python
python 美化输出信息的实例
Oct 15 Python
Python中使用logging和traceback模块记录日志和跟踪异常
Apr 09 Python
python 列表转为字典的两个小方法(小结)
Jun 28 Python
python中while和for的区别总结
Jun 28 Python
Python函数生成器原理及使用详解
Mar 12 Python
浅谈python中的多态
Jun 15 Python
python 对图片进行简单的处理
Jun 23 Python
如何利用Python实现一个论文降重工具
Jul 09 Python
bat批处理之字符串操作的实现
Mar 16 Python
Pandas读取并修改excel的示例代码
Feb 17 #Python
Python实现去除列表中重复元素的方法总结【7种方法】
Feb 16 #Python
Python字符串逆序输出的实例讲解
Feb 16 #Python
强悍的Python读取大文件的解决方案
Feb 16 #Python
Python基础之文件读取的讲解
Feb 16 #Python
解决Python3 被PHP程序调用执行返回乱码的问题
Feb 16 #Python
Python3 修改默认环境的方法
Feb 16 #Python
You might like
我的论坛源代码(九)
2006/10/09 PHP
修改ThinkPHP缓存为Memcache的方法
2014/06/25 PHP
php发送get、post请求的6种方法简明总结
2014/07/08 PHP
php集成环境xampp中apache无法启动问题解决方案
2014/11/18 PHP
php中数字、字符与对象判断函数用法实例
2014/11/26 PHP
PHP实现的简单网络硬盘
2015/07/29 PHP
微信利用PHP创建自定义菜单的方法
2016/08/01 PHP
Yii实现微信公众号场景二维码的方法实例
2020/08/30 PHP
Laravel配合jwt使用的方法实例
2020/10/25 PHP
JS BASE64编码 window.atob(), window.btoa()
2021/03/09 Javascript
在textarea文本域中显示HTML代码的方法
2007/03/06 Javascript
JQuery 插件模板 制作jquery插件的朋友可以参考下
2010/03/17 Javascript
jQuery阻止事件冒泡具体实现
2013/10/11 Javascript
JS显示下拉列表框内全部元素的方法
2015/03/31 Javascript
jQuery动画显示和隐藏效果实例演示(附demo源码下载)
2015/12/31 Javascript
JavaScript数据结构与算法之栈与队列
2016/01/29 Javascript
node.js实现回调的方法示例
2017/03/01 Javascript
基于BootStrap的文本编辑器组件Summernote
2017/10/27 Javascript
Vue 使用formData方式向后台发送数据的实现
2019/04/14 Javascript
Vue 理解之白话 getter/setter详解
2019/04/16 Javascript
js代码实现轮播图
2020/05/04 Javascript
[01:12:35]Spirit vs Navi Supermajor小组赛 A组败者组第一轮 BO3 第二场 6.2
2018/06/03 DOTA
Python的高级Git库 Gittle
2014/09/22 Python
使用Python的PIL模块来进行图片对比
2016/02/18 Python
python开发简易版在线音乐播放器
2017/03/03 Python
Python远程视频监控程序的实例代码
2019/05/05 Python
Django forms表单 select下拉框的传值实例
2019/07/19 Python
一个精品风格的世界:Atterley
2019/05/01 全球购物
英国Flybe航空官网:欧洲最大的独立支线廉价航空公司
2019/07/15 全球购物
Java语言的优势
2015/01/10 面试题
2015届本科毕业生自我鉴定
2014/09/27 职场文书
法律讲堂观后感
2015/06/11 职场文书
2015年中学图书馆工作总结
2015/07/22 职场文书
一篇文章弄懂MySQL查询语句的执行过程
2021/05/07 MySQL
《极主夫道》真人电影正式预告 定档6月3日上映
2022/04/05 日漫
MySQL时区造成时差问题
2022/04/13 MySQL