不要用强制方法杀掉python线程


Posted in Python onFebruary 26, 2017

前言:

    不要试图用强制方法杀掉一个python线程,这从服务设计上就存在不合理性。 多线程本用来任务的协作并发,如果你使用强制手段干掉线程,那么很大几率出现意想不到的bug。  请记住一点,锁资源不会因为线程退出而释放锁资源 !

我们可以举出两个常见的例子:

1. 有个A线程拿到了锁,因为他是被强制干掉的,没能及时的release()释放锁资源,那么导致所有的线程获取资源是都被阻塞下去,这就是典型的死锁场景。

2.在常见的生产消费者的场景下,消费者从任务队列获取任务,但是被干掉后没有把正在做的任务丢回队列中,那么这就造成了数据丢失。

下面是java和python终止线程的方法:

java有三种方法可以使终止线程:

1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2. 使用stop方法强行终止线程(不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。
3. 使用interrupt方法中断线程。

python可以有两种方法:

1. 退出标记
2. 使用ctypes强行杀掉线程

不管是python还是java环境下,理想的停止退出线程方法是 让线程自个自杀,所谓的线程自杀就是 你给他一个标志位,他退出线程。

下面我们会采用多种方法来测试 停止python线程的异常情况。我们查看一个进程所有的执行线程,  进程是用过掌控资源,线程是用作调度单元,进程要被调度执行必须要有一个线程,默认的线程和进程的pid一样的。

ps -mp 31449 -o THREAD,tid
 
USER   %CPU PRI SCNT WCHAN USER SYSTEM  TID
root   0.0  -  - -     -   -   -
root   0.0 19  - poll_s  -   - 31449
root   0.0 19  - poll_s  -   - 31450

获取到了进程所有的线程后,通过strace得知 31450 是需要我们kill的线程id,当我们kill的时候,会出现整个进程都崩溃的情况。 在多线程环境下,产生的信号是传递给整个进程的,一般而言,所有线程都有机会收到这个信号,进程在收到信号的的线程上下文执行信号处理函数,具体是哪个线程执行的难以获知。也就是说,信号会随机发个该进程的一个线程。

strace -p <span style="font-size:14px;line-height:21px;">31450</span> Process <span style="font-size:14px;line-height:21px;">31450</span> attached - interrupt to quit
select(0, NULL, NULL, NULL, {0, 320326}) = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})   = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})   = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})   = ? ERESTARTNOHAND (To be restarted)
--- SIGTERM (Terminated) @ 0 (0) ---
Process <span style="font-size:14px;line-height:21px;">31450</span> detached

上面出现的问题其实跟pthread的说明是一致的。当我们在python代码里加入 signal 信号处理函数后,回调函数可以防止整个进程的退出,那么问题来了,通过信号函数不能识别你要干掉哪一个线程,也就是说,不能精准的干掉某个线程。你虽然把信号发给31450线程id,但是信号受理人是所属进程的任何一个,另外传给信号处理函数的参数只有信号数和信号stack而已,可有可无的。

加了信号处理后,不会退出进程

select(0, NULL, NULL, NULL, {1, 0})   = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})   = ? ERESTARTNOHAND (To be restarted)
--- SIGTERM (Terminated) @ 0 (0) ---
rt_sigreturn(0xffffffff)        = -1 EINTR (Interrupted system call)
select(0, NULL, NULL, NULL, {1, 0})   = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})   = 0 (Timeout)

如果想从外部通知杀掉某个线程,那么可以构建使用rpc服务,或者别的方式通信,signal信号不可以,因为无法无法传递更多的信息。

python的线程不是模拟的,是真实的内核线程,内核调用pthread方法,但Python上层没有提供关闭线程的方法,这就需要我们自己把握了。强烈推荐使用 event 或者 自定义标志位的方法, 如果非要强制杀掉线程,那么可以用python ctypes PyThreadState SetAsyncExc 方法强制退出,这样对于运行的python服务没有什么影响。

该函数的实现原理比较简单,其实也是在python虚拟机里做个标示位,然后由虚拟机运行一个异常来取消线程,虚拟机会帮你做好try cache。 切记不要在外部杀掉python的某个线程,虽然你能通过ctypes找到线程id,但是你直接kill会干掉整个进程的。

下面的代码是 用ctypes 杀掉线程的样例,不推荐使用,因为太粗暴了.

import ctypes
 
def terminate_thread(thread):
  if not thread.isAlive():
    return
 
  exc = ctypes.py_object(SystemExit)
  res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
    ctypes.c_long(thread.ident), exc)
  if res == 0:
    raise ValueError("nonexistent thread id")
  elif res > 1:
    ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, None)
    raise SystemError("PyThreadState_SetAsyncExc failed")

咱们简单look一下PyThreadState源代码,总而言之触发线程的异常模式。 有兴趣的人可以阅读 python pystate.c 的设计,配合着youtube的一些视频分享。

int
PyThreadState_SetAsyncExc(long id, PyObject *exc) {
  PyInterpreterState *interp = GET_INTERP_STATE();
  ...
  HEAD_LOCK();
  for (p = interp->tstate_head; p != NULL; p = p->next) {
    if (p->thread_id == id) {
      从链表里找到线程的id,避免死锁,我们需要释放head_mutex。
      PyObject *old_exc = p->async_exc;
      Py_XINCREF(exc); #增加该对象的引用数
      p->async_exc = exc; # 更为exc模式
      HEAD_UNLOCK();
      Py_XDECREF(old_exc); # 因为要取消,当然也就递减引用
      ...
      return 1; #销毁线程成功
    }
  }
  HEAD_UNLOCK();
  return 0;
}

原生posix pthread 可以使用 ptread_cancel(tid) 在主线程中结束子线程。但是 Python 的线程库不支持这样做,理由是我们不应该强制地结束一个线程,这样会带来很多隐患,应该让该线程自己结束自己。所以在 Python 中,推荐的方法是在子线程中循环判断一个标志位,在主线程中改变该标志位,子线程读到标志位改变,就结束自己。

类似这个逻辑:

def consumer_threading():
 t1_stop= threading.Event()
 t1 = threading.Thread(target=thread1, args=(1, t1_stop))
 
 t2_stop = threading.Event()
 t2 = threading.Thread(target=thread2, args=(2, t2_stop))
 
 time.sleep(duration)
 #stop the thread2
 t2_stop.set()
 
def thread1(arg1, stop_event):
 while(not stop_event.is_set()):
   #similar to time.sleep()
   stop_event.wait(time)
   pass
 
 
def thread2(arg1, stop_event):
 while(not stop_event.is_set()):
   stop_event.wait(time)
   pass

简单的总结,虽然我们可以用ctypes里的pystats来控制线程,但这种粗暴中断线程的方法是不合理的。 请选用 自杀模式 !如果你的线程正在发生io阻塞,而不能判断事件怎么办? 你的程序需要做优化了,最少在网络io层需要有主动的timeout,避免一直的阻塞下去。

Python 相关文章推荐
Python日期操作学习笔记
Oct 07 Python
Python向日志输出中添加上下文信息
May 24 Python
运用TensorFlow进行简单实现线性回归、梯度下降示例
Mar 05 Python
Python实现对文件进行单词划分并去重排序操作示例
Jul 10 Python
Django rest framework工具包简单用法示例
Jul 20 Python
实例讲解python中的协程
Oct 08 Python
对python同一个文件夹里面不同.py文件的交叉引用方法详解
Dec 15 Python
详解pandas的外部数据导入与常用方法
May 01 Python
使用Python画出小人发射爱心的代码
Nov 23 Python
在tensorflow中实现屏蔽输出的log信息
Feb 04 Python
Python爬虫谷歌Chrome F12抓包过程原理解析
Jun 04 Python
pycharm配置python 设置pip安装源为豆瓣源
Feb 05 Python
Python实现 多进程导入CSV数据到 MySQL
Feb 26 #Python
python检查URL是否正常访问的小技巧
Feb 25 #Python
python解析基于xml格式的日志文件
Feb 25 #Python
Python中防止sql注入的方法详解
Feb 25 #Python
Python 数据结构之旋转链表
Feb 25 #Python
Python数据结构之翻转链表
Feb 25 #Python
浅析python中SQLAlchemy排序的一个坑
Feb 24 #Python
You might like
PHP异步调用socket实现代码
2012/01/12 PHP
PHP中time(),date(),mktime()区别介绍
2013/09/28 PHP
javascript 图片上传预览-兼容标准
2009/06/01 Javascript
读jQuery之十一 添加事件核心方法
2011/07/31 Javascript
jquery 实现表单验证功能代码(简洁)
2012/07/03 Javascript
js 关键词高亮(根据ID/tag高亮关键字)案例介绍
2013/01/21 Javascript
原生js操作checkbox用document.getElementById实现
2013/10/12 Javascript
xmlhttp缓存清除的2种解决方法
2013/12/13 Javascript
JS通过分析userAgent属性来判断浏览器的类型及版本
2014/03/28 Javascript
node.js中的fs.readlink方法使用说明
2014/12/17 Javascript
浅谈jQuery构造函数分析
2015/05/11 Javascript
jquery实现初次打开有动画效果的网页TAB切换代码
2015/09/06 Javascript
jQuery实现的登录浮动框效果代码
2015/09/26 Javascript
又一款js时钟!transform实现时钟效果
2016/08/15 Javascript
js 能实现监听F5页面刷新子iframe 而父页面不刷新的方法
2016/11/09 Javascript
JavaScript简单生成 N~M 之间随机数的方法
2017/01/13 Javascript
Bootstrap多级菜单的实现代码
2017/05/23 Javascript
在vscode里使用.vue代码模板的方法
2018/04/28 Javascript
Easyui 关闭jquery-easui tab标签页前触发事件的解决方法
2019/04/28 jQuery
vue项目中播放rtmp视频文件流的方法
2020/09/17 Javascript
Python中运行并行任务技巧
2015/02/26 Python
使用Python操作Elasticsearch数据索引的教程
2015/04/08 Python
Python基于回溯法子集树模板解决野人与传教士问题示例
2017/09/11 Python
Python KMeans聚类问题分析
2018/02/23 Python
python实现的登录与提交表单数据功能示例
2019/09/25 Python
Python中求对数方法总结
2020/03/10 Python
Python HTTP下载文件并显示下载进度条功能的实现
2020/04/02 Python
Python sql注入 过滤字符串的非法字符实例
2020/04/03 Python
基于jupyter代码无法在pycharm中运行的解决方法
2020/04/21 Python
Python的历史与优缺点整理
2020/05/26 Python
菲律宾旅游网站:Expedia菲律宾
2017/10/11 全球购物
员工工作表现评语
2014/04/26 职场文书
公证委托书格式
2014/09/13 职场文书
2015年机关党委工作总结
2015/05/23 职场文书
小学少先队工作总结2015
2015/05/26 职场文书
学子宴致辞大全
2015/07/27 职场文书