python在线程中的exec'd脚本中断无限循环

时间:2014-08-18 14:14:37

标签: python asynchronous abort

GUI应用程序允许用户编写Python脚本,然后运行它(通过exec)。问题是如果用户错误地(我不关心恶意,只是用户方面的诚实编码错误)脚本包含无限循环,控件将永远不会返回到我的应用程序。由于GUI,键盘中断不起作用。

我已经确定了四种处理方法:

  1. 跟踪:使用sys.settrace,以便在每一行调用一个函数;在那里,我可以设置逻辑,试图识别是否一遍又一遍地执行相同的代码段(在实践中我将不知道这有多么具有挑战性)。我猜这可能会大大降低执行速度。
  2. 异步异常:在一个单独的线程中运行脚本,并使用ctypes让主线程使用PyThreadState_SetAsyncExc在脚本线程中引发一个异常,使其脱离循环(参见https://gist.github.com/liuw/2407154和{{3} })。然后调用exec的线程部分将恢复控制并可以采取适当的操作(向用户显示消息等)。我知道,线程不应该从外部中止,但是在这里,即使这样做会使脚本在未定义状态下被更改的对象留下,至少它会允许GUI丢弃不再可靠的对象(它可以做到这一点,因为脚本可以访问的所有修改都在一个可以抛出并从磁盘重新加载的大对象中)。
  3. 单独进程:使用多处理模块,将在单独的进程中运行脚本;但是,该脚本可以包含对父级主线程中存在的对象的调用,因此这将变得非常复杂。
  4. 守护程序线程:在守护程序线程中运行脚本,然后如果脚本doean在一段时间后返回,请考虑它"挂起"。当应用程序退出时,线程将继续运行,用户可以通过任务管理器强行终止它(或者通过引发SystemException等退出线程)。
  5. 所以问题:如果我必须提供这个功能,上述哪种方法是所有邪恶中的较小者?还有其他技术,不是上面的4吗?

4 个答案:

答案 0 :(得分:1)

一种方法是使用自定义multiprocessing.Manager对象。它可以为您处理同步。 (链接:multiprocessing

这是一个拥有单个实例的示例,其中多进程进程能够在其上调用方法。注意我没有使用实例状态,这留给了读者:)

原始代码正在传递" maths.add" (实例方法)到池,但方法是不可选择的。因此,我创建了全局" my_add",它接受一个数学实例( 可选),然后将一些数字加在一起并产生结果。

from multiprocessing import Pool
from multiprocessing.managers import BaseManager

class MathsClass(object):
    def add(self, x, y):
        return x + y
    def mul(self, x, y):
        return x * y

class MyManager(BaseManager):
    pass

MyManager.register('Maths', MathsClass)

def my_add(mobj, *args):
    return mobj.add(*args)

if __name__ == '__main__':
    manager = MyManager()
    manager.start()
    maths = manager.Maths()

    # pass the shared 'maths' object into each process

    pool = Pool()
    print pool.apply(my_add, [maths, 4, 3])
    # print maths.add(4, 3)         # prints 7
    print pool.apply(my_add, [maths, 7, 8])
    # print maths.mul(7, 8)         # prints 56

输出

7
15

答案 1 :(得分:1)

如何让Python代码保持主线程并产生一个工作线程,该线程将睡眠,然后用SIGINT中断主线程。

当用户进程完成时,您可以终止该工作程序。但是如果工作人员触发,你将获得相当于键盘中断的功能,这样你就可以捕获产生的异常并进行清理。

这样你就没有内存问题与主程序共享你的东西了,但你仍然会在一段固定的时间后以一种杀死失控的子程序的方式被戳戳。

答案 2 :(得分:0)

一般情况下,选项1被贬低,选项2或4和1会很好地结合起来。

你可以生成一个线程来安装你的settrace回调,并继续在某种try / catch包装器中加载和执行脚本代码模块。

settrace的实现不需要尝试分析,只需检查自己的线程的年龄,如果它太旧,则抛出异常。

然后包装器代码可以通知GUI超时。

我想这里的一部分原因是您可能不想直接使用exec来执行用户的脚本,而是将模块加载到您exec的内容中,这样您就可以打包它周围的Python代码,包括try / except块,以及安装可以注入提示的settrace或Timer线程等。

答案 3 :(得分:0)

除非你事先知道将要执行的代码到底是什么,使用#1 settrace来确定代码是否实际上仍处于活动状态,这将是困难的,是的会减慢执行代码的速度,可能会相当大,取决于代码的用途。请参阅halting problem

选项#3是最好的(与#4半结合)。这就是单独的流程所针对的 - 分开工作,同时让第一个流程与其他事情相结合。设置并不困难。

新进程(P2)不必包含对GUI中对象的调用,应该将注意事项分开。这并不意味着他们无法互动。使用套接字对进行通信,例如,使用pickle双方将python对象相互发送。 GUI主循环应该每个例如安排检查。 50ms,用于检查其套接字(设置为非阻塞),以便从P2进行通信。然后,P2可以向GUI发送消息(如果愿意,GUI将响应该消息)。

P2在新进程P3中执行代码(不是绝对必要,但更好的设置)。 P2还检查来自GUI的命令(如果需要,定期检查),诸如“停止执行脚本”之类的命令。当在GUI中使用停止按钮时,如果使用os.kill(P3ID..)等,P2可以执行P3.terminate()multiprocessing。或者它也可以向GUI发送命令并接收回它需要的数据,GUI将在最多50ms内响应(开始响应)。

下面的代码只是部分片段,根本没有经过测试,只是为了让您了解架构。最好分成不同的部分,例如用于包装每个套接字的SocketCom类,它封装了使用pickle将数据转换为字节,并使用底层套接字发送它,或者以阻塞或非阻塞模式接收单个消息select,使用底层套接字在消息传回之前接收和取消消息,等等。

一般代码

sGUI, s2 = socket.socketpair(socket.AF_UNIX, socket.SEQPACKET)
sGUI.setblocking(False) # for direct use, but will raise system-dependent errors, better to use select
P2 = multiprocessing.Process(target=P2process, args=(s2,codeToRun) )

def P2process (sock, codeToRun) :
    # sock is the socket connected to GUI socket
    P3 = multiprocessing.Process(target=P3process, args=(codeToRun,) ) # note, args is tuple
    # block (up to you) for messages from/to GUI, check P3 periodically, etc.
    # e.g. can do :
    sock.sendall(pickle.dumps("sendMeX"))
    X = pickle.loads(sock.recv(length))
    # or e.g. a blocking loop that responds only to messages from GUI :
    while True :
        msg = pickle.loads(sock.recv(length))
        if msg == 'status' :
            sock.sendall(pickle.dumps(P3.is_alive()))
        elif msg == 'stop' :
            P3.terminate()
        elif msg == 'newExecCode' :
            newExecCode = pickle.loads(sock.recv(length))
        elif msg == 'quit' :
            P3.terminate()
            break
    ...

def P3process (codeToRun) :
    exec(codeToRun) # you should sandbox its context with custom globals & locals
    # up to you how to solve the halting problem ;)
    # user probably decides and GUI has 'stop' button if exec takes too long

对于GUI:

GUI只要使用sGUI,就可以向P2发送命令。点击按钮。要侦听来自P2的消息,它看起来像这样:

def GUI_P2Com_loop (self) :
    # this is called once, and at the end of the function, it registers itself as a
    # callback to be called again after a timeout. It checks for messages from P2, 
    # and responds as necessary. It can also launch other processes to respond instead.
    try :
        reads, w, x = select.select([sGUI], [], [], 0) # 0 = non-blocking
        if sGUI in reads :
            msg = pickle.loads(sGUI.recv(length))
            # got message from P2, do whatever
            if msg == 'sendMeX' :
                sGUI.sendall(pickle.dumps(X))
    ...

    # so that the GUI can get on with responding to user interaction
    # all GUI frameworks should have a function for registering a callback after timeout
    # e.g. a tkinter widget would call :
    self._job = widget.after(50, GUI_P2Com_loop) # where widget could be self if class extends widget

存储_job,以便可以例如取消通信循环。 tkinter再次:

def cancel_com_loop (self) :
    self.after_cancel(self._job)

请参阅tkinter effbot documentation