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

在线程中是否有一种优雅的或python式的方式来中断time.sleep()调用?,python,multithreading,Python,Multithreading,下面的代码按照我期望的方式工作,即: 有一个从1到8计数的QThread(“Ernie”),在计数之间休眠1秒 有一个免费的UI小部件(“Bert”) 在正常操作下,程序运行直到线程完成并且UI关闭 Ctrl-C键盘中断将在正常完成之前正常停止程序 要做到这一点,我必须将1秒睡眠分解为50毫秒的块,以检查标志 有没有一种更像python的方式可以在线程中睡眠一段时间(例如1秒),但可以被某个标志或信号打断? try: for i in xrange(8

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

  • 有一个从1到8计数的QThread(“Ernie”),在计数之间休眠1秒
  • 有一个免费的UI小部件(“Bert”)
  • 在正常操作下,程序运行直到线程完成并且UI关闭
  • Ctrl-C键盘中断将在正常完成之前正常停止程序
要做到这一点,我必须将1秒睡眠分解为50毫秒的块,以检查标志

有没有一种更像python的方式可以在线程中睡眠一段时间(例如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)

我的本能是用os.kill发出信号,但只有主线程接收信号,所以Ernie不能被那样打断。文档建议改为使用锁

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

由于您无法按我们所希望的方式集成信号和线程,下面是另一篇关于线程锁定超时的帖子:


我无法告诉你这个解决方案有多像蟒蛇,因为我仍在努力理解蟒蛇到底意味着什么。一旦你开始引入线程,优雅的代码就变得越来越难写了,无论如何。

我不确定它有多像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)

通常,
SIGINT
会中断
time.sleep
调用,但Python只允许应用程序的主线程接收信号,因此不能在这里使用。我建议尽可能避免
时间。睡眠
,并使用
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)
coroutine
允许修饰函数在出现
yield
时,在给定的时间内将控制权交还给Qt事件循环,然后恢复修饰方法的执行。诚然,这实际上只是改变了我最初示例的复杂性,但它确实将复杂性隐藏在您试图在线程中执行的实际工作之外

工作原理:

该方法受异步库(如和模块)中的协同路由实现的启发。虽然我没有尝试提出像这些一样强大的东西,但想法是一样的。我们希望能够中断的方法被实现为生成器,并用一个decorator进行修饰,该decorator知道如何以允许正确暂停/恢复生成器的方式调用和接收来自生成器的响应。调用
do\u print
时的流程基本上如下所示:

  • do\u print()
    是从
    run
    调用的。这实际上会导致调用
    coroutine.wrapper
  • wrapper
    调用实际的
    do\u print
    ,它返回一个生成器对象。它将该对象传递给
    execute
  • execute
    调用生成器对象上的
    next
    。这将导致
    do_print
    运行,直到达到
    产量
    。然后暂停执行
    do_print
  • execute
    schedules
    do\u print
    恢复执行。它首先通过使用上次运行的
    do\u print
    迭代中的值
    yield
    ed,或者通过默认值0(将执行安排为立即恢复)来确定何时进行调度。它调用
    QTimer.singleShot
    来计划自己在
    timeout
    毫秒内再次运行,使用
    partial
    ,这样它也可以传递生成器对象
  • 重复步骤3-4,直到
    do\u print
    停止让步,调用
    self.exit()
    并返回,此时会引发
    StopIteration
    ,而
    coroutine
    装饰程序只是返回,而不是安排另一个
    execute
    调用

  • +1由于队列是一种已知的健壮的进程间/线程间通信机制,我明白你的意思,但实现细节似乎使其过于复杂。如果我能处理协作多路复用或状态机方法,我根本不会为线程而烦恼。。。但是它使设计变得简单易懂。@JasonS QT项目的网站实际上明确指出,使用线程和
    睡眠
    来实现计时器是一种很好的方法。他们基本上主张使用
    QTimer
    ,而不引入
    QThread
    。因此,对于您的特定示例,我认为首选的方法是完全避免
    QThread
    。当然,如果你正在做一些CPU密集型的事情,这会改变一切。好吧,这里的“sleep()”调用实际上是任何类型的长操作的代理,这些长操作不受CPU限制,也不容易从内到外转换为事件驱动的方法:在复杂的事件序列中等待网络数据、等待I/O等。是的,我可以使用QTimer和状态机,但状态机方法使设计/测试/审查/等变得非常糟糕。所以,是的,从并发性的角度来看,QTimer方法会更好,但特定领域的优先级胜过这一点。@JasonS公平。我对我的答案进行了编辑,加入了一种基于协同程序的方法,这种方法可能会更好地满足您的需求,因为它将使用计时器的大部分复杂性转移到别处
    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"