【问题标题】:Is there a graceful or Pythonic way to interrupt a time.sleep() call in a thread?是否有一种优雅或 Pythonic 的方式来中断线程中的 time.sleep() 调用?
【发布时间】:2014-07-07 18:18:17
【问题描述】:

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

  • 有一个 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)

【问题讨论】:

    标签: python multithreading


    【解决方案1】:

    我不确定它是如何 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 因为队列是一种已知的强大的进程间/线程间通信机制
    【解决方案2】:

    通常,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's 事件循环立即关闭。

    编辑:

    这是实现相同想法的另一种方法,它至少隐藏了一些复杂性,允许原始 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"    
    

    每当yield 出现时,coroutine 允许修饰函数在给定时间内将控制权交还给 Qt 事件循环,然后继续执行修饰方法。诚然,这实际上只是从我的原始示例中转移了复杂性,但它确实将其隐藏在您尝试在线程中执行的实际工作之外。

    工作原理:

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

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

    【讨论】:

    • 我明白你的意思,但实施细节似乎使这过于复杂。如果我可以处理协作多路复用或状态机方法,我根本不会打扰线程......但它使设计易于理解。
    • @JasonS QT 项目的网站实际上明确指出使用线程和sleep 来实现计时器是bad idea。他们基本上提倡使用QTimer,根本不引入QThread。因此,对于您的具体示例,我认为首选方法是完全避免QThread。当然,如果你正在做一些 CPU 密集型的事情,那就改变了。
    • 好的,这里的“sleep()”调用实际上是任何类型的长操作的代理,它不受 CPU 限制并且不能轻易地从内到外变成事件驱动方式:在复杂的事件序列中等待网络数据、等待I/O等。是的,我可以使用 QTimer 和状态机,但是状态机方法让设计/测试/审查/等变得非常糟糕。
    • 所以是的,从并发的角度来看,QTimer 方法会更好,但特定领域的优先级胜过这一点。
    • @JasonS 够公平的。我已经编辑了我的答案以包含一种基于协程的方法,它可能会更好地满足您的要求,因为它将使用计时器的大部分复杂性从线程中的主要逻辑转移到了装饰器中。
    【解决方案3】:

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

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

    由于您无法按照我们想要的方式集成信号和线程,这里有另一篇关于线程锁定超时的文章:

    How to implement a Lock with a timeout in Python 2.7

    我无法告诉您这个解决方案的 Pythonic 程度如何,因为我仍在尝试了解 Pythonic 究竟意味着什么。一旦你开始引入线程,无论如何,优雅的代码会变得越来越难写。

    【讨论】:

      猜你喜欢
      • 2015-07-10
      • 2014-02-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-06-11
      • 1970-01-01
      • 2011-02-23
      • 2019-12-04
      相关资源
      最近更新 更多