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

6
以下代码按照预期工作:
- 有一个名为"Ernie"的QThread,从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个回答

8

我不确定这个方法是否符合Python规范,但它是可行的。只需要使用队列并使用带有超时的阻塞式get方法即可。请看下面的示例:

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 因为队列是已知的强大的进程/线程间通信机制。 - Jason S

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时,流程基本上是这样的:
1.从run中调用do_print(),实际上会导致调用coroutine.wrapper。 2.wrapper调用真正的do_print,它返回一个生成器对象。它将该对象传递给execute。 3.execute在生成器对象上调用next。这导致do_print运行,直到出现yield。然后暂停执行do_print。 4.execute安排do_print恢复执行。它首先通过使用从上一次运行的do_printyield的值或默认为0(将立即恢复执行)来确定何时安排它。它使用partial调用QTimer.singleShot,以便可以同时传递生成器对象。 5.步骤3-4重复,直到do_print停止产生yield,调用self.exit()并返回,在此时引发StopIterationcoroutine装饰器只需返回而不是安排另一个execute调用。

我理解你的观点,但实现细节似乎使得这变得过于复杂。如果我可以使用协作多路复用或状态机方法来处理,我根本不需要考虑线程...但这使得设计更易于理解。 - Jason S
@JasonS QT项目的网站实际上明确指出,使用线程和sleep来实现定时器是一个不好的想法。他们基本上提倡完全不使用QThread,而是使用QTimer。所以对于你的具体示例,我认为最好的方法是完全避免使用QThread。当然,如果你正在做一些CPU密集型的工作,那就另当别论了。 - dano
好的,这里的“sleep()”调用实际上是任何类型的长时间操作的代理,它既不受CPU限制,也不能轻松地转换为事件驱动的方法:等待网络数据、等待I/O等等。是的,我可以使用QTimer和状态机,但状态机方法会使设计/测试/审查等变得非常混乱。 - Jason S
所以,从并发性的角度来看,QTimer方法会更好,但特定于域的优先级胜过这一点。 - Jason S
哇 - 我需要看看那个并弄清楚发生了什么。看起来非常有趣! - Jason S
显示剩余2条评论

1
我的直觉是使用 os.kill 发出信号,但只有主线程会接收到信号,所以 Ernie 不能通过这种方式被中断。文档建议使用锁(lock)代替。
我的想法是创建一个只能在关闭 Ernie 时访问的锁。在主线程创建 Bert 和 Ernie 后,创建并锁定锁文件。然后,Ernie 不会睡眠一秒钟,而是花费整整一秒钟的时间尝试获取锁。一旦程序需要关闭,您可以释放锁,Ernie 将立即获得锁;这告诉 Ernie 是时候关闭了。
由于无法像我们想要的那样集成信号和线程,因此这里有另一篇关于线程锁超时的帖子: 如何在 Python 2.7 中实现具有超时的锁(Lock) 我无法告诉您这个解决方案有多符合 Python 的规范,因为我仍在努力理解 Pythonic 到底意味着什么。无论如何,一旦开始引入线程,优雅的代码编写变得越来越困难。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接