当前位置:脚本大全 > > 正文

python多进程与多线程详解(Python线程之定位与销毁的实现)

时间:2022-03-29 03:59:06类别:脚本大全

python多进程与多线程详解

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

背景

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

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

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

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

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

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

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

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

    销毁指定线程

    既然能拿到名字和线程 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,触发指定线程的异常,让其可以自动退出;

    python多进程与多线程详解(Python线程之定位与销毁的实现)

    为什么停止线程这么难

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

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

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

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

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

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

    如果是单纯的线程,其实系统是有办法终止的,比如: pthread_exit,pthread_killpthread_cancel, 详情可看:

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

    如何温柔退出线程

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

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

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

    原文链接:https://segmentfault.com/a/1190000018177256

    上一篇下一篇

    猜您喜欢

    热门推荐