背景

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

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

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

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

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

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

找出线程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()

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@---:~# python t.py
pid:
OOOOOPPPPP
pid:
EEEEEEEEE
...

可以看到在 Python 里面输出的线程名就是我们设置的那样,然而 Ps 的结果却是令我怀疑人生:

root@---:~# ps -Tp
  PID  SPID TTY          TIME CMD
    pts/    :: python
    pts/    :: python
    pts/    :: 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);

    )
    {
        printf("pid: %d, thread_id: %u, t_name: %s\n", pid, tid, tname);
        sleep();
    }
}

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@---:~# gcc t.c -lpthread && ./a.out
pid: , thread_id: , t_name: Love_test_2
pid: , thread_id: , t_name: Love_test_1
pid: , thread_id: , t_name: Love_test_2
pid: , thread_id: , t_name: Love_test_1
...

用 PS 命令再次验证:

root@---:~# ps -Tp
  PID  SPID TTY          TIME CMD
    pts/    :: a.out
    pts/    :: Love_test_1
    pts/    :: 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

    @name.setter
    def name(self, name):
        assert self.__initialized, "Thread.__init__() not called"
        self.__name = str(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()

def tt():
    info = threading.currentThread()
    pid = os.getpid()
    while True:
        print 'pid: {}, tid: {}, tname: {}'.format(pid, info.name, info.ident)
        time.sleep()
        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@---:~# python t_show.py
pid: , tid: Thread-test1, tname:
pid: , tid: Thread-test2, tname: 

------- Running threads On Pid:  -------
MainThread
Thread-test1
Checker
Thread-test2 

------- Running threads On Pid:  -------
MainThread
Thread-test1
Checker
Thread-test2 

------- Running threads On Pid:  -------
MainThread
Thread-test1
Checker
Thread-test2 

------- Running threads On Pid:  -------
MainThread
Checker
...

代码看起来有点长,但是逻辑相当简单,Thread-test1 和 Thread-test2 都是打印出当前的 pid、线程 id 和 线程名字,然后 3s 后退出,这个是想模拟线程正常退出。

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

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

销毁指定线程

既然能拿到名字和线程 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))
    :
        raise ValueError("invalid thread id")
    elif res != :
        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()

输出

root@---:~# python t_show.py
pid: , tid: , tname: Thread-test1
pid: , tid: , tname: Thread-test2
------- Running threads On Pid:  -------
MainThread  True
Thread-test1  True
Checker  True
Thread-test2  True
Thread-test2: I am go dying. Please take care of yourself and drink more hot water!

------- Running threads On Pid:  -------
MainThread  True
Thread-test1  True
Checker  True
Thread-test2  True
Thread-test2: I am go dying. Please take care of yourself and drink more hot water!

pid: , tid: , tname: Thread-test1
------- Running threads On Pid:  -------
MainThread  True
Thread-test1  True
Checker  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, 详情可看:https://www.cnblogs.com/Creat...

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

如何温柔退出线程

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

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

扩展

《如何正确的终止正在运行的子线程》:https://www.cnblogs.com/Creat...
《不要粗暴的销毁python线程》:http://xiaorui.cc/2017/02/22/...

来源: https://segmentfault.com/a/11...

51Reboot 教育最新课程通知

  • Python 零基础入门课程
  • Python 运维自动化进阶课程
  • Docker + K8s 课程

早报名可享受早鸟价

想要详细了解和报名的同学可以扫码添加好友私聊啦

Python:线程之定位与销毁的更多相关文章

  1. python——线程与多线程进阶

    之前我们已经学会如何在代码块中创建新的线程去执行我们要同步执行的多个任务,但是线程的世界远不止如此.接下来,我们要介绍的是整个threading模块.threading基于Java的线程模型设计.锁( ...

  2. python 线程(一)理论部分

    Python线程 进程有很多优点,它提供了多道编程,可以提高计算机CPU的利用率.既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的. 主要体现在一下几个方面: 进程只能在 ...

  3. Python 线程和进程和协程总结

    Python 线程和进程和协程总结 线程和进程和协程 进程 进程是程序执行时的一个实例,是担当分配系统资源(CPU时间.内存等)的基本单位: 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其 ...

  4. [ python ] 线程的操作

    目录 (见右侧目录栏导航) - 1. 前言    - 1.1 进程    - 1.2 有了进程为什么要有线程    - 1.3 线程的出现    - 1.4 进程和线程的关系    - 1.5 线程的 ...

  5. python线程池ThreadPoolExecutor(上)(38)

    在前面的文章中我们已经介绍了很多关于python线程相关的知识点,比如 线程互斥锁Lock / 线程事件Event / 线程条件变量Condition 等等,而今天给大家讲解的是 线程池ThreadP ...

  6. python — 线程

    目录 1.线程基础知识 2 Thread 类 3 锁 4 队列 1.线程基础知识 1.1 进程与线程的区别 进程: 创建进程 时间开销大 销毁进程 时间开销大 进程之间切换 时间开销大 线程: 线程是 ...

  7. Python线程池与进程池

    Python线程池与进程池 前言 前面我们已经将线程并发编程与进程并行编程全部摸了个透,其实我第一次学习他们的时候感觉非常困难甚至是吃力.因为概念实在是太多了,各种锁,数据共享同步,各种方法等等让人十 ...

  8. 5分钟看懂系列:Python 线程池原理及实现

    概述 传统多线程方案会使用"即时创建, 即时销毁"的策略.尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器 ...

  9. python——线程与多线程基础

    我们之前已经初步了解了进程.线程与协程的概念,现在就来看看python的线程.下面说的都是一个进程里的故事了,暂时忘记进程和协程,先来看一个进程中的线程和多线程.这篇博客将要讲一些单线程与多线程的基础 ...

随机推荐

  1. C#获取H5页面上传图片代码

    基于上一篇的H5压缩上传图片,由于图片是以二进制字符流blob的形式传过来的,所以应该想将其转成bytes类型再进行转换 public void ProcessRequest(HttpContext ...

  2. 查看python版本

    1.未进入python shell python --version 2.进入python shell,有两种方法 (1) help() (2) import sys sys.version

  3. 关于MySQL checkpoint

    Ⅰ.Checkpoint 1.1 checkpoint的作用 缩短数据库的恢复时间 缓冲池不够用时,将脏页刷到磁盘 重做日志不可用时,刷新脏页 1.2 展开分析 page被缓存在bp中,page在bp ...

  4. Python面试题(一)【转】

    注:本面试题来源于网络,转载自http://www.cnblogs.com/goodhacker/p/3366618.html. 1. (1)python下多线程的限制以及多进程中传递参数的方式 py ...

  5. Tell Me About Yourself Example 1

    Tell Me About Yourself Employers ask this question because they want to know you have the relevant e ...

  6. 教你优化yum源。配置阿里云的yum镜像源(base和epel)

    一.Centos7的base源配置阿里云的yum源: 1.备份旧的yum源目录下的所有文件 [root@ELK-chaofeng07 yum.repos.d]# mkdir ../yum.repos. ...

  7. win10系统如何关掉系统自动更新

    越来越多的电脑使用者都在使用Windows10系统,尽管系统是一代代更新的,但难免有槽点,Windows10系统也不例外,最大的槽点就是“自动更新”的功能.当然,“自动更新”的功能也是相当有用处的.  ...

  8. asp.net core中使用HttpClient实现Post和Get的同步异步方法

     准备工作 1.visual studio 2015 update3开发环境 2.net core 1.0.1 及以上版本  目录 1.HttpGet方法 2.HttpPost方法 3.使用示例 4. ...

  9. IO流(字节流,字符流,缓冲流)

    一:IO流的分类(组织架构) 根据处理数据类型的不同分为:字节流和字符流 根据数据流向不同分为:输入流和输出流   这么庞大的体系里面,常用的就那么几个,我们把它们抽取出来,如下图:   二:字符字节 ...

  10. 【洛谷】【最小生成树】P1536 村村通

    [题目描述:] 某市调查城镇交通状况,得到现有城镇道路统计表.表中列出了每条道路直接连通的城镇.市政府"村村通工程"的目标是使全市任何两个城镇间都可以实现交通(但不一定有直接的道路 ...