探寻python多线程ctrl+c退出问题解决方案

734次阅读  |  发布于5年以前

场景:

经常会遇到下述问题:很多io busy的应用采取多线程的方式来解决,但这时候会发现python命令行不响应ctrl-c 了,而对应的java代码则没有问题:

复制代码 代码如下:

public class Test {
public static void main(String[] args) throws Exception {

    new Thread(new Runnable() {    

        public void run() {    
            long start = System.currentTimeMillis();    
            while (true) {    
                try {    
                    Thread.sleep(1000);    
                } catch (Exception e) {    
                }    
                System.out.println(System.currentTimeMillis());    
                if (System.currentTimeMillis() - start > 1000 * 100) break;    
            }    
        }    
    }).start();    

}    

}
java Test

ctrl-c则会结束程序

而对应的python代码:

复制代码 代码如下:

-- coding: utf-8 --

import time
import threading
start=time.time()
def foreverLoop():
start=time.time()
while 1:
time.sleep(1)
print time.time()
if time.time()-start>100:
break

thread_=threading.Thread(target=foreverLoop)

thread_.setDaemon(True)

thread_.start()

python p.py

后ctrl-c则完全不起作用了。

不成熟的分析:

首先单单设置 daemon 为 true 肯定不行,就不解释了。当daemon为 false 时,导入python线程库后实际上,threading会在主线程执行完毕后,检查是否有不是 daemon 的线程,有的化就wait,等待线程结束了,在主线程等待期间,所有发送到主线程的信号也会被阻测,可以在上述代码加入signal模块验证一下:

复制代码 代码如下:

def sigint_handler(signum,frame):
print "main-thread exit"
sys.exit()
signal.signal(signal.SIGINT,sigint_handler)

在100秒内按下ctrl-c没有反应,只有当子线程结束后才会出现打印 "main-thread exit",可见 ctrl-c被阻测了

threading 中在主线程结束时进行的操作:

复制代码 代码如下:

_shutdown = _MainThread()._exitfunc
def _exitfunc(self):
self._Thread__stop()
t = _pickSomeNonDaemonThread()
if t:
if debug:
self._note("%s: waiting for other threads", self)
while t:
t.join()
t = _pickSomeNonDaemonThread()
if debug:
self._note("%s: exiting", self)
self._Thread__delete()

对所有的非daemon线程进行join等待,其中join中可自行察看源码,又调用了wait,同上文分析 ,主线程等待到了一把锁上。

不成熟的解决:

只能把线程设成daemon才能让主线程不等待,能够接受ctrl-c信号,但是又不能让子线程立即结束,那么只能采用传统的轮询方法了,采用sleep间歇省点cpu吧:

复制代码 代码如下:

-- coding: utf-8 --

import time,signal,traceback
import sys
import threading
start=time.time()
def foreverLoop():
start=time.time()
while 1:
time.sleep(1)
print time.time()
if time.time()-start>5:
break

thread=threading.Thread(target=foreverLoop)
thread
.setDaemon(True)
thread_.start()

主线程wait住了,不能接受信号了

thread_.join()

def exitCheckfunc():
print "ok"
try:
while 1:
alive=False
if thread
.isAlive():
alive=True
if not alive:
break
time.sleep(1)

为了使得统计时间能够运行,要捕捉 KeyboardInterrupt :ctrl-c

except KeyboardInterrupt, e:    
    traceback.print_exc()    
print "consume time :",time.time()-start    

threading._shutdown=_exitCheckfunc

缺点:轮询总会浪费点cpu资源,以及battery.

有更好的解决方案敬请提出。

ps1: 进程监控解决方案 :

用另外一个进程来接受信号后杀掉执行任务进程,牛

复制代码 代码如下:

-- coding: utf-8 --

import time,signal,traceback,os
import sys
import threading
start=time.time()
def foreverLoop():
start=time.time()
while 1:
time.sleep(1)
print time.time()
if time.time()-start>5:
break

class Watcher:
"""this class solves two problems with multithreaded
programs in Python, (1) a signal might be delivered
to any thread (which is just a malfeature) and (2) if
the thread that gets the signal is waiting, the signal
is ignored (which is a bug).

The watcher is a concurrent process (not thread) that   
waits for a signal and the process that contains the   
threads.  See Appendix A of The Little Book of Semaphores.   
<http://greenteapress.com/semaphores/>   

I have only tested this on Linux.  I would expect it to   
work on the Macintosh and not work on Windows.   
"""    

def __init__(self):    
    """ Creates a child thread, which returns.  The parent   
        thread waits for a KeyboardInterrupt and then kills   
        the child thread.   
    """    
    self.child = os.fork()    
    if self.child == 0:    
        return    
    else:    
        self.watch()    

def watch(self):    
    try:    
        os.wait()    
    except KeyboardInterrupt:    
        # I put the capital B in KeyBoardInterrupt so I can    
        # tell when the Watcher gets the SIGINT    
        print 'KeyBoardInterrupt'    
        self.kill()    
    sys.exit()    

def kill(self):    
    try:    
        os.kill(self.child, signal.SIGKILL)    
    except OSError: pass    

Watcher()
thread=threading.Thread(target=foreverLoop)
thread
.start()

注意 watch()一定要放在线程创建前,原因未知。。。。,否则立刻就结束

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8