是否有一种优雅或Pythonic方式来中断线程中的time.sleep()调用?

时间:2014-07-07 18:18:18

标签: python multithreading

下面的代码按我期望的方式工作,即:

  • 有一个QThread(“Ernie”)从1到8计数,在计数之间睡1秒
  • 有一个无偿的UI小部件(“Bert”)
  • 在正常操作下,程序运行直到线程完成并且UI关闭
  • Ctrl-C键盘中断将在正常完成之前正常停止程序。

要做到这一点,我不得不将1秒钟的睡眠时间分成50毫秒的块状物来检查旗帜。

是否有更多的Pythonic方式在线程中休眠一段时间(例如1秒)但是可以被某些标志或信号中断?

        try:
            for i in xrange(8):
                print "i=%d" % i
                for _ in xrange(20):
                    time.sleep(0.05)
                    if not self.running:
                        raise GracefulShutdown
        except GracefulShutdown:
            print "ernie exiting"        

我宁愿这样做,并以某种方式在线程中引起GracefulShutdown异常:

        try:
            for i in xrange(8):
                print "i=%d" % i
                time.sleep(1)
                # somehow allow another thread to raise GracefulShutdown
                # during the sleep() call
        except GracefulShutdown:
            print "ernie exiting"        

完整的程序;

from PySide import QtCore, QtGui
from PySide.QtGui import QApplication
import sys
import signal
import time

class GracefulShutdown(Exception):
    pass

class Ernie(QtCore.QThread):
    def __init__(self):
        super(Ernie, self).__init__()
        self.running = True
    def run(self):
        try:
            for i in xrange(8):
                print "i=%d" % i
                for _ in xrange(20):
                    time.sleep(0.05)
                    if not self.running:
                        raise GracefulShutdown
        except GracefulShutdown:
            print "ernie exiting"        
    def shutdown(self):
        print "ernie received request to shutdown"
        self.running = False

class Bert(object):
    def __init__(self, argv):
        self.app = QApplication(argv)
        self.app.quitOnLastWindowClosed = False
    def show(self):
        widg = QtGui.QWidget()
        widg.resize(250, 150)
        widg.setWindowTitle('Simple')
        widg.show()
        self.widg = widg
        return widg
    def shutdown(self):
        print "bert exiting"
        self.widg.close()
    def start(self):
        # return control to the Python interpreter briefly every 100 msec
        timer = QtCore.QTimer()
        timer.start(100)
        timer.timeout.connect(lambda: None) 
        return self.app.exec_()        

def handleInterrupts(*actors):
    def handler(sig, frame):
        print "caught interrupt"
        for actor in actors:
            actor.shutdown()
    signal.signal(signal.SIGINT, handler)

bert = Bert(sys.argv)
gratuitousWidget = bert.show()
ernie = Ernie()
ernie.start()

handleInterrupts(bert, ernie)

retval = bert.start()
print "bert finished"
while not ernie.wait(100):
    # return control to the Python interpreter briefly every 100 msec
    pass
print "ernie finished"
sys.exit(retval)

3 个答案:

答案 0 :(得分:6)

我不确定Pythonic是如何但它有效。只需使用队列并使用阻塞获取超时。请参阅以下示例:

import threading
import Queue
import time

q = Queue.Queue()


def workit():
    for i in range(10):
        try:
            q.get(timeout=1)
            print '%s: Was interrupted' % time.time()
            break
        except Queue.Empty:
            print '%s: One second passed' % time.time()


th = threading.Thread(target=workit)
th.start()

time.sleep(3.2)
q.put(None)

答案 1 :(得分:3)

通常情况下,SIGINT会中断time.sleep调用,但Python只允许应用程序主线程接收信号,因此无法在此处使用。如果可能,我建议避免使用time.sleep,而是使用QTimer

from PySide import QtCore, QtGui
from PySide.QtCore import QTimer
from PySide.QtGui import QApplication
import sys 
import signal
from functools import partial

class Ernie(QtCore.QThread):
    def __init__(self):
        super(Ernie, self).__init__()

    def do_print(self, cur_num, max_num):
        print "i=%d" % cur_num
        cur_num += 1
        if cur_num < max_num:
            func = partial(self.do_print, cur_num, max_num)
            QTimer.singleShot(1000, func)
        else:
            self.exit()

    def run(self):
        self.do_print(0, 8)
        self.exec_()  # QTimer requires the event loop for the thread be running.
        print "ernie exiting"    


class Bert(object):
    def __init__(self, argv):
        self.app = QApplication(argv)
        self.app.quitOnLastWindowClosed = False
    def show(self):
        widg = QtGui.QWidget()
        widg.resize(250, 150)
        widg.setWindowTitle('Simple')
        widg.show()
        self.widg = widg
        return widg
    def shutdown(self):
        print "bert exiting"
        self.widg.close()
    def start(self):
        # return control to the Python interpreter briefly every 100 msec
        timer = QtCore.QTimer()
        timer.start(100)
        timer.timeout.connect(lambda: None) 
        return self.app.exec_()            

def handleInterrupts(*actors):
    def handler(sig, frame):
        print "caught interrupt"
        for actor in actors:
            actor.shutdown()
    signal.signal(signal.SIGINT, handler)

bert = Bert(sys.argv)
gratuitousWidget = bert.show()
ernie = Ernie()
ernie.start()

handleInterrupts(bert)

retval = bert.start()
print "bert finished"
ernie.exit()
while not ernie.wait(100):
    # return control to the Python interpreter briefly every 100 msec
    pass
print "ernie finished"
sys.exit(retval)

我们不是让run()方法运行带time.sleep的for循环,而是在线程内部启动一个事件循环,并使用QTimer以设定的间隔进行所需的打印。这样,只要我们希望线程关闭,我们就可以调用bernie.exit(),这将导致bernie'事件循环立即关闭。

修改

这是另一种实现同样想法的方法,至少隐藏了一些复杂性,允许原始for循环保持不变:

def coroutine(func):
    def wrapper(*args, **kwargs):
        def execute(gen):
            try:
                op = gen.next() # run func until a yield is hit
                # Determine when to resume execution of the coroutine.
                # If func didn't yield anything, schedule it to run again
                # immediately by setting timeout to 0.
                timeout = op or 0 
                func = partial(execute, gen)
                QTimer.singleShot(timeout, func) # This schedules execute to run until the next yield after `timeout` milliseconds.
            except StopIteration:
                return

        gen = func(*args, **kwargs) # Get a generator object for the decorated function.
        execute(gen) 
    return wrapper

def async_sleep(timeout):
    """ When yielded inside a coroutine, triggers a `timeout` length sleep. """
    return timeout

class Ernie(QtCore.QThread):
    def __init__(self):
        super(Ernie, self).__init__()
        self.cur_num = 0 
        self.max_num = 8 

    @coroutine
    def do_print(self):
        for i in range(8):
            print "i=%d" % i 
            yield async_sleep(1000) # This could also just be yield 1000
        self.exit()

    def run(self):
        self.do_print() # Schedules do_print to run once self.exec_() is run.
        self.exec_()
        print "ernie exiting"    

coroutine允许装饰函数在yield出现时将控制权返回给Qt事件循环一段给定的时间,然后继续执行装饰方法。当然,这实际上只是将复杂性从我原来的例子中移除,但它确实使它远离你在线程中尝试做的实际工作。

工作原理:

该方法的灵感来自异步库中的协程实现,例如Tornadoasyncio模块。虽然我没有尝试提出像那些强大的东西,但这个想法是一样的。我们希望能够中断的方法被实现为生成器,并且装饰有装饰器,装饰器知道如何以允许正确暂停/恢复生成器的方式调用和接收来自生成器的响应。调用do_print时的流程基本上是这样的:

    do_print()调用
  1. run。这实际上会导致coroutine.wrapper被调用。
  2. wrapper调用真实的do_print,它返回一个生成器对象。它将该对象传递给execute
  3. execute在生成器对象上调用next。这会导致do_print一直运行,直到yield被击中。然后暂停执行do_print
  4. execute安排do_print恢复执行。它通过首先确定何时调度它来执行此操作,方法是使用运行的yield的上一次迭代中的值do_print,或者通过默认为0(将执行安排为立即恢复)。它调用QTimer.singleShot来安排自己使用timeoutpartial毫秒内再次运行,以便它也可以传递生成器对象。
  5. 步骤3-4重复,直到do_print停止产生,调用self.exit()并返回,此时StopIteration被引发,coroutine装饰器只返回而非调度另一个execute电话。

答案 2 :(得分:1)

我的直觉是用os.kill发出一个信号,但只有主线接收到信号,所以厄尼不会被这样打断。文档建议使用锁定。

我的想法是创造一个只有在杀死厄尼的时候才能进入的锁。在主线程创建Bert和Ernie之后,会创建并锁定锁定文件。然后,厄尼将花费一整秒的时间试图获得锁定,而不是睡一秒钟。一旦程序关闭,您就可以释放锁定,Ernie会立即获得锁定;这告诉厄尼,是时候关闭了。

由于您无法按照我们喜欢的方式集成信号和线程,因此这里有另一篇文章讨论线程锁定超时:

How to implement a Lock with a timeout in Python 2.7

我无法告诉你这个解决方案是如何Pythonic的,因为我仍然想要了解Pythonic究竟是什么。一旦你开始介绍线程,优雅的代码在任何情况下都会变得越来越难写。