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

背景

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

我们有个了不起的后台程序,可以动态加载模块,并以线程方式运行,通过这种形式实现插件的功能。而模块更新时候,后台程序自身不会退出,只会将模块对应的线程关闭、更新代码再启动,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,触发指定线程的异常,让其可以自动退出;

为什么停止线程这么难

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

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

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

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

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

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

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

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

如何温柔退出线程

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

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

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

(0)

相关推荐

  • python通过线程实现定时器timer的方法

    本文实例讲述了python通过线程实现定时器timer的方法.分享给大家供大家参考.具体分析如下: 这个python类实现了一个定时器效果,调用非常简单,可以让系统定时执行指定的函数 下面介绍以threading模块来实现定时器的方法. 使用前先做一个简单试验: import threading def sayhello(): print "hello world" global t #Notice: use global variable! t = threading.Timer(5

  • python杀死一个线程的方法

    最近在项目中遇到这一需求: 我需要一个函数工作,比如远程连接一个端口,远程读取文件等,但是我给的时间有限,比如,4秒钟如果你还没有读取完成或者连接成功,我就不等了,很可能对方已经宕机或者拒绝了.这样可以批量做一些事情而不需要一直等,浪费时间. 结合我的需求,我想到这种办法: 1.在主进程执行,调用一个进程执行函数,然后主进程sleep,等时间到了,就kill 执行函数的进程. 测试一个例子: import time import threading def p(i): print i class

  • 理解python多线程(python多线程简明教程)

    对于python 多线程的理解,我花了很长时间,搜索的大部份文章都不够通俗易懂.所以,这里力图用简单的例子,让你对多线程有个初步的认识. 单线程 在好些年前的MS-DOS时代,操作系统处理问题都是单任务的,我想做听音乐和看电影两件事儿,那么一定要先排一下顺序. (好吧!我们不纠结在DOS时代是否有听音乐和看影的应用.^_^) 复制代码 代码如下: from time import ctime,sleep def music():    for i in range(2):        prin

  • Python多线程编程(二):启动线程的两种方法

    在Python中我们主要是通过thread和threading这两个模块来实现的,其中Python的threading模块是对thread做了一些包装的,可以更加方便的被使用,所以我们使用threading模块实现多线程编程.一般来说,使用线程有两种模式,一种是创建线程要执行的函数,把这个函数传递进Thread对象里,让它来执行:另一种是直接从Thread继承,创建一个新的class,把线程执行的代码放到这个新的 class里. 将函数传递进Thread对象 复制代码 代码如下: '''  Cr

  • Python多线程编程(八):使用Event实现线程间通信

    使用threading.Event可以实现线程间相互通信,之前的Python:使用threading模块实现多线程编程七[使用Condition实现复杂同步]我们已经初步实现了线程间通信的基本功能,但是更为通用的一种做法是使用threading.Event对象.使用threading.Event可以使一个线程等待其他线程的通知,我们把这个Event传递到线程对象中,Event默认内置了一个标志,初始值为False.一旦该线程通过wait()方法进入等待状态,直到另一个线程调用该Event的set

  • 详解Python中的多线程编程

    一.简介 多线程编程技术可以实现代码并行性,优化处理能力,同时功能的更小划分可以使代码的可重用性更好.Python中threading和Queue模块可以用来实现多线程编程. 二.详解 1.线程和进程        进程(有时被称为重量级进程)是程序的一次执行.每个进程都有自己的地址空间.内存.数据栈以及其它记录其运行轨迹的辅助数据.操作系统管理在其上运行的所有进程,并为这些进程公平地分配时间.进程也可以通过fork和spawn操作来完成其它的任务,不过各个进程有自己的内存空间.数据栈等,所以只

  • 浅析Python中的多进程与多线程的使用

    在批评Python的讨论中,常常说起Python多线程是多么的难用.还有人对 global interpreter lock(也被亲切的称为"GIL")指指点点,说它阻碍了Python的多线程程序同时运行.因此,如果你是从其他语言(比如C++或Java)转过来的话,Python线程模块并不会像你想象的那样去运行.必须要说明的是,我们还是可以用Python写出能并发或并行的代码,并且能带来性能的显著提升,只要你能顾及到一些事情.如果你还没看过的话,我建议你看看Eqbal Quran的文章

  • Python threading多线程编程实例

    Python 的多线程有两种实现方法: 函数,线程类 1.函数 调用 thread 模块中的 start_new_thread() 函数来创建线程,以线程函数的形式告诉线程该做什么 复制代码 代码如下: # -*- coding: utf-8 -*- import thread def f(name):   #定义线程函数   print "this is " + name   if __name__ == '__main__':   thread.start_new_thread(f

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

    背景 开工前我就觉得有什么不太对劲,感觉要背锅.这可不,上班第三天就捅锅了. 我们有个了不起的后台程序,可以动态加载模块,并以线程方式运行,通过这种形式实现插件的功能.而模块更新时候,后台程序自身不会退出,只会将模块对应的线程关闭.更新代码再启动,6 得不行. 于是乎我就写了个模块准备大展身手,结果忘记写退出函数了,导致每次更新模块都新创建一个线程,除非重启那个程序,否则那些线程就一直苟活着. 这可不行啊,得想个办法清理呀,要不然怕是要炸了. 那么怎么清理呢?我能想到的就是两步走: 找出需要清理

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

    前言: 不要试图用强制方法杀掉一个python线程,这从服务设计上就存在不合理性. 多线程本用来任务的协作并发,如果你使用强制手段干掉线程,那么很大几率出现意想不到的bug.  请记住一点,锁资源不会因为线程退出而释放锁资源 ! 我们可以举出两个常见的例子: 1. 有个A线程拿到了锁,因为他是被强制干掉的,没能及时的release()释放锁资源,那么导致所有的线程获取资源是都被阻塞下去,这就是典型的死锁场景. 2.在常见的生产消费者的场景下,消费者从任务队列获取任务,但是被干掉后没有把正在做的任

  • 解决python线程卡死的问题

    1. top命令和日志方式判定卡死的位置 python代码忽然卡死,日志不输出,通过如下方式可以确定线程确实已经死掉了: # top 命令 top命令可以看到机器上所有线程的执行情况,%CPU和%MEM可以看出线程消耗的资源情况 由于机器上线程数量太多,可能要查看的线程的信息在top命令当前屏幕上显示不出来可以通过如下方式查看 在top命令下输入:u 接下来会提示输入用户名,就可以查看该用户所执行的所有线程 Which user (blank for all): denglinjie 这样就可以

  • Python线程threading模块用法详解

    本文实例讲述了Python线程threading模块用法.分享给大家供大家参考,具体如下: threading-更高级别的线程接口 源代码:Lib/threading.py 该模块在较低级别thread模块之上构建更高级别的线程接口.另请参见mutex和Queue模块. 该dummy_threading模块适用于threading因thread缺失而无法使用的情况 . 注意: 从Python 2.6开始,该模块提供 符合 PEP 8的别名和属性,以替换camelCase受Java的线程API启发

  • 实例代码讲解Python 线程池

    大家都知道当任务过多,任务量过大时如果想提高效率的一个最简单的方法就是用多线程去处理,比如爬取上万个网页中的特定数据,以及将爬取数据和清洗数据的工作交给不同的线程去处理,也就是生产者消费者模式,都是典型的多线程使用场景. 那是不是意味着线程数量越多,程序的执行效率就越快呢. 显然不是.线程也是一个对象,是需要占用资源的,线程数量过多的话肯定会消耗过多的资源,同时线程间的上下文切换也是一笔不小的开销,所以有时候开辟过多的线程不但不会提高程序的执行效率,反而会适得其反使程序变慢,得不偿失. 所以,如

  • Python线程池的正确使用方法

    目录 Python线程池的正确使用 1.为什么要使用线程池呢? 2.线程池怎么用呢? 3.如何非阻塞的获取线程执行的结果 4.线程池的运行策略 Python线程池的正确使用 1.为什么要使用线程池呢? 因为线程执行完任务之后就会被系统销毁,下次再执行任务的时候再进行创建.这种方式在逻辑上没有啥问题.但是系统启动一个新线程的成本是比较高,因为其中涉及与操作系统的交互,操作系统需要给新线程分配资源.打个比方吧!就像软件公司招聘员工干活一样.当有活干时,就招聘一个外包人员干活.当活干完之后就把这个人员

  • Python线程编程之Thread详解

    目录 一.线程编程(Thread) 1.线程基本概念 1.1.什么事线程 1.2.线程特征 二.threading模块创建线程 1.创建线程对象 2. 启动线程 3. 回收线程 4.代码演示 5.线程对象属性 6.自定义线程类 7.一个很重要的练习 我很多不懂 8.线程间通信 1. 线程Event 代码演示 2. 线程锁 Lock代码演示 10.死锁及其处理 1.定义 2.图解 3. 死锁产生条件 4.死锁代码演示 python线程GIL 1.python线程的GIL问题 (全局解释器锁) 总结

  • Python线程池thread pool创建使用及实例代码分享

    目录 前言 一.线程 1.线程介绍 2.线程特性 轻型实体 独立调度和分派的基本单位 可并发执行 4)共享进程资源 二.线程池 三.线程池的设计思路 四.Python线程池构建 1.构建思路 2.实现库功能函数 ThreadPoolExecutor() submit() result() cancel() cancelled() running() as_completed() map() 前言 首先线程和线程池不管在哪个语言里面,理论都是通用的.对于开发来说,解决高并发问题离不开对多个线程处理

  • 浅谈一下python线程池简单应用

    一.线程池简介 传统多线程方案会使用“即时创建,即时销毁”的策略.尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务时执行时间较短,而且执行次数及其频繁,那么服务器将处于不停的创建线程,销毁线程的状态. 一个线程的运行时间可以分为三部分:线程的启动时间.线程体的运行时间和线程的销毁时间. 在多线程处理的情景中,如果线程不能被重用,就意味着每次线程运行都要经过启动.销毁和运行3个过程.这必然会增加系统相应的时间,减低了效率. 线程池在系统启动时即创建大量空闲的线程,程序只要

  • Golang与python线程详解及简单实例

    Golang与python线程详解及简单实例 在GO中,开启15个线程,每个线程把全局变量遍历增加100000次,因此预测结果是 15*100000=1500000. var sum int var cccc int var m *sync.Mutex func Count1(i int, ch chan int) { for j := 0; j < 100000; j++ { cccc = cccc + 1 } ch <- cccc } func main() { m = new(sync.

随机推荐