如何在多次调用时停止长时间运行的函数?

时间:2016-04-19 21:05:51

标签: python multithreading python-3.x pyqt pyqt4

下面我有一个示例程序。按下按钮时,它需要一秒钟才能计算要显示的值。如果用户快速连续按下按钮,他们最终会等待很长时间才能看到最后一个答案,这是他们唯一关心的答案。在代码中,您可以看到_dataCruncher函数需要知道self._count,但self._count不依赖于_dataCruncher的输出。

因此,我的问题是,如何在后续调用中中断_dataCruncher的正常执行,以保持GUI自由执行其他操作,并且在不需要时不浪费处理时间?我意识到我可能需要使用一个线程来运行_dataCruncher和某种Queue以获得相应的val来显示,但我不明白如何将它们放在一起。

from PyQt4 import QtGui, QtCore
import sys
import time
import random
import random

class MainWindow(QtGui.QMainWindow):
    def __init__(self):
        self.app = QtGui.QApplication(sys.argv)
        super(MainWindow, self).__init__()
        self.count = 0
        self.initUI()

    def initUI(self):
        # Layouts
        central = QtGui.QWidget()
        layout = QtGui.QVBoxLayout()

        self.button = QtGui.QPushButton('Press Me')
        self.text = QtGui.QLabel('?')

        layout.addWidget(self.button)
        layout.addWidget(self.text)

        central.setLayout(layout)
        self.setCentralWidget(central)

        self.button.clicked.connect(self._buttonClicked)

    def _dataCruncher(self, val):
        time.sleep(1) # takes a long time to process data using val
        return val * random.randint(1,10)

    def _buttonClicked(self):
        self.count += 1
        val = self._dataCruncher(self.count)
        self.text.setText('Value {}'.format(val))

    def startup(self):
        self.show()
        result = self.app.exec_()
        sys.exit(result)

if __name__ == '__main__':
    random.seed()
    myWindow = MainWindow()
    myWindow.startup()

2 个答案:

答案 0 :(得分:0)

所以,找到答案比我想象的要复杂得多。正如@MTset在其中一条评论中提到的那样,python没有提供取消执行Thread的任何方法。所以,我所做的是创建一个&threadlerHandler'那个,好吧,处理线程。它跟踪创建的最后一个线程,并提供了从最后一个线程的执行中获取结果的方法。

我正在发布原始帖子的测试代码的修改版本以及完整的threadHandler代码,以防任何人使用它。

文件1在这里

# tester.py, run this file

from PyQt4 import QtGui, QtCore
import random, sys, time
from threadHandler import MyThreadHandler

class MyModel(object):
    def dataCruncher(self, val):
        delay = random.randint(1,5)
        print('{} sleeping for {}'.format(val, delay))
        time.sleep(delay) # takes a long time to process data using val
        print('{} done sleeping'.format(val))
        return val

class MainWindow(QtGui.QMainWindow):
    def __init__(self, threadHandler):
        self.app = QtGui.QApplication(sys.argv)
        super(MainWindow, self).__init__()
        self.count = 0
        self.initUI()
        self.button_clicked_events = Event()
        self.threadHandler = threadHandler

    def initUI(self):
        # Layouts
        central = QtGui.QWidget()
        layout = QtGui.QVBoxLayout()

        self.button = QtGui.QPushButton('Press Me')
        self.text = QtGui.QLabel('?')

        layout.addWidget(self.button)
        layout.addWidget(self.text)

        central.setLayout(layout)
        self.setCentralWidget(central)

        self.button.clicked.connect(self._buttonClicked)

    def _buttonClicked(self):
        self.count += 1
        self.button_clicked_events(self.count)

    def setLabel(self, val):
        self.text.setText(str(val))

    def startup(self):
        self.show()
        result = self.app.exec_()
        return result


class Event(list):
    """Event subscription.

    A list of callable objects. Calling an instance of this will cause a
    call to each item in the list in ascending order by index.

    Example Usage:
    >>> def f(x):
    ...     print 'f(%s)' % x
    >>> def g(x):
    ...     print 'g(%s)' % x
    >>> e = Event()
    >>> e()
    >>> e.append(f)
    >>> e(123)
    f(123)
    >>> e.remove(f)
    >>> e()
    >>> e += (f, g)
    >>> e(10)
    f(10)
    g(10)
    >>> del e[0]
    >>> e(2)
    g(2)

    """
    def __init__(self):
        self.output = {}

    def __call__(self, *args, **kwargs):
        for f,key in self:
            output = f(*args, **kwargs)
            self.output[key] = output
        return self.output
    def __repr__(self):
        return "Event({})".format(list.__repr__(self))

if __name__ == '__main__':
    def checker(handler, window):
        if handler.isLastDone():
            val = handler.getLastResult()
            window.setLabel(val)
        else:
            window.setLabel('calculating...')

    random.seed()
    model = MyModel()
    threadHandler = MyThreadHandler()
    myWindow = MainWindow(threadHandler)

    threadHandler.createTimer(1, checker, threadHandler, myWindow)

    def getData(count):
        threadHandler.createOneShot(model.dataCruncher, count)

    myWindow.button_clicked_events.append((getData, 'dt'))

    result = myWindow.startup()
    print('ending')
    threadHandler.end()
    print('ended')
    sys.exit(result)

下面的文件2

#threadHandler.py, save this file in the same folder as tester.py

import threading, time

class MyThreadHandler(object):
    def __init__(self):
        self.oneShots = []
        self.timers = []
        self.oldOneShots = []
        self.latest = None
        self.cleaning = False

        self._startCleaner()

    def _startCleaner(self):
        print('-'*20+'Starting cleaner'+'-'*20)
        self.cleaner = self.createTimer(1, self._cleanupThreads)

    def _stopCleaner(self):
        print('-'*20+'Stopping cleaner'+'-'*20)
        self.cleaner.stop()

    def getNumThreads(self):
        return len(self.oneShots)

    def getNumOldThreads(self):
        return len(self.oldOneShots)

    def end(self):
        for i,timer in enumerate(self.timers):
            timer.stop()
            self.timers.pop(i)

    def createTimer(self, interval, func, *args, **kwargs):
        timer = myTimer(interval, func, args, kwargs)
        self.timers.append(timer)
        return timer

    def createOneShot(self, func, *args, **kwargs):
        oneshot = myOneShot(func, args, kwargs)
        self.oneShots.append(oneshot)
        self.latest = oneshot

    def isLastDone(self):
        if not self.latest is None:
            return not self.latest.running()
        else:
            return None

    def getLastResult(self):
        if self.latest is None:
            raise ValueError('There have not been any oneshots created.')
        while self.latest.running():
            pass
        result = self.latest.getResult()
        if len(self.oneShots) > 0:
            self.oldOneShots.append(myOneShot(self._cleanAll, (self.oneShots,)))
        self.oneShots = []
        return result

    def _cleanAll(self, toClean):
        # loop through toClean and pop up anything that's done. this DOES lock
        while len(toClean) > 0:
            toClean = self._cleanup(toClean)

    def _cleanup(self, toCleanup):
        while not self.cleaning:
            self.cleaning = True
            for i, thread in enumerate(toCleanup):
                if not thread.running():
                    toCleanup.pop(i)
        self.cleaning = False
        return toCleanup

    def _cleanupThreads(self):
        # check each of these lists and pop out any threads that are done. This
        # does not lock. This function should really only be called by the
        # cleaner, which is set up in __init__
        self.oneShots = self._cleanup(self.oneShots)
        self.timers = self._cleanup(self.timers)
        self.oldOneShots = self._cleanup(self.oldOneShots)

class myTimer(object):
    def __init__(self, delay, func, args=tuple(), kwargs={}):
        self.delay  = delay
        self.func   = func
        self.loop   = True
        self.args   = args
        self.kwargs = kwargs
        self.thread = threading.Thread(target=self.run, daemon=True)
        self.thread.start()
        self.output = None

    def run(self):
        while self.loop:
            self.output = self.func(*self.args, **self.kwargs)
            if self.delay > 0.1:
                count = 0
                while count <= self.delay:
                    count += 0.1
                    time.sleep(0.1)
            else:
                time.sleep(self.delay)

    def stop(self):
        self.loop = False

    def running(self):
        return self.loop

    def getResult(self):
        return self.output

class myOneShot(object):
    def __init__(self, func, args=tuple(), kwargs={}):
        self.func = func
        self.args = args
        self.kwargs = kwargs
        self.thread = threading.Thread(target=self.run, daemon=True)
        self.thread.start()
        self.output = None

    def run(self):
        self.output = self.func(*self.args, **self.kwargs)

    def running(self):
        return self.thread.is_alive()

    def getResult(self):
        return self.output

if __name__ == '__main__':
    import random
    random.seed()

    def longFunc(num):
        delay = random.randint(5,8)
        if num in (3, 6):
            delay = 2
        print('-'*30+'func {} has sleep {}'.format(num, delay))
        time.sleep(delay)
        print('-'*30+'func {} is done'.format(num))
        return num

    def checker(handler):
        if handler.isLastDone():
            return handler.getLastResult()
        else:
            return None

    myHandler = MyThreadHandler()

    # The 'checker' function simulates something in my program that uses the
    # data generated by the 'longFunc'. It waits until there are no more threads
    # in the threadHandler, as that would indicate that the user is done
    # switching back-and-forth between different values
    checkTimer = myHandler.createTimer(1, checker, myHandler)

    # create 10 one-shot threads that take a 'long' time. The delay is to keep
    # them in order, as this loop is meant to simulate a user switching between
    # items using a keyboard or mouse, which I imagine they couldn't do any
    # faster than every 1/10th of a second
    start = time.time()
    for i in range(4):
        myHandler.createOneShot(longFunc, i)
        time.sleep(0.1)

    # wait until there are no more threads executing
    last = myHandler.getLastResult()

    print('result from last = {}'.format(last))

    for i in range(4, 7):
        myHandler.createOneShot(longFunc, i)
        time.sleep(0.1)

    last = myHandler.getLastResult()
    print('result from last = {}'.format(last))

    while myHandler.getNumOldThreads() >0 or myHandler.getNumThreads() > 0:
        pass

    myHandler.end()
    print('done ending')

答案 1 :(得分:-1)

您可以在按下按钮后禁用按钮,直到答案准备就绪:     的setEnabled(假) 然后在提供结果之前重置它。