【问题标题】:python interrupt infinite loop in exec'd script in threadpython在线程中的exec'd脚本中中断无限循环
【发布时间】:2014-08-18 14:14:36
【问题描述】:

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

我已经确定了四种方法来处理这个问题:

  1. 跟踪:使用 sys.settrace 以便在每一行调用一个函数;在那里,我可以放置逻辑来尝试识别是否一遍又一遍地执行同一段代码(这在实践中会有多大的挑战,我还不知道)。我猜这可能会大大降低执行速度。
  2. 异步异常:在单独的线程中运行脚本,并使用 ctypes 让主线程使用 PyThreadState_SetAsyncExc 在脚本线程中引发异常以将其踢出循环(参见 https://gist.github.com/liuw/2407154http://tomerfiliba.com/recipes/Thread2/)。然后调用 exec 的线程部分将恢复控制并可以采取适当的措施(向用户显示消息等)。我知道,线程不应该从外部中止,但是在这里,即使这样做会使脚本更改的对象处于未定义状态,至少它会允许 GUI 丢弃不再可靠的对象(它可以做到这一点,因为脚本可访问的所有修改都在一个大对象中,可以从磁盘中抛出并重新加载)。
  3. 独立进程:使用多处理模块,将在独立进程中运行脚本;但是脚本可以包含对存在于父主线程中的对象的调用,因此这将变得相当复杂。
  4. 守护线程:在守护线程中运行脚本,如果脚本在一段时间后没有返回,则认为它“挂起”。当应用程序退出时,线程将继续运行,用户可以通过任务管理器强行杀死它(或者退出线程可以通过引发 SystemException 等)。

所以问题是:如果我必须提供此功能,以上哪种方法是万恶之源?是否还有其他技术,不在上述 4 中?

【问题讨论】:

  • 我认为您的主要问题是您允许用户脚本访问位于主应用程序命名空间中的对象。提供用户脚本可以访问的托管 API 将允许您使其线程/进程安全,从而在单独的进程中运行该脚本没什么大不了的。 :)
  • @JoelCornett 您能否说明线程安全如何使我能够中断无限循环?在 Python shell 中,您可以 ctrl-c 无限循环,因为它在主线程中运行。但是这里主线程运行 GUI,第二个线程运行脚本;他们也只能在第二个线程中访问对象。除非我遗漏了什么,否则这里不涉及线程安全。问题是我无法在主线程中运行脚本,在那里可以中断无限循环(break 或 SIGINT 等),并且我没有“标准”方式来中断非主线程。
  • 如果你在一个单独的进程中运行代码,你可以给它发送中断信号。通过线程/进程安全,我的意思是实现一种 IPC 机制,以允许在用户脚本进程和主进程之间发生 API 调用。
  • @JoelCornett 啊,这与我的问题的第 3 项有何不同?

标签: python asynchronous abort


【解决方案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

【讨论】:

  • 感谢您的回答。但这与我的问题的备选方案 3 有何不同?
  • 我认为这是一个类似于原始评论员的建议,即在#3 中,最好将东西传递到您的流程中,在一些经理门面后面,而不是真正分享。当然,如果要与主进程拥有的那些对象的更改同步,这很困难。 (所以我认为到目前为止,您已经对 #3 投了三票,并试图保证分享并没有那么糟糕。)
  • +1 如果数学类具有状态,并且其状态的变化会自动反映在进程中,那么这可能是一个有趣的选择,尽管在 main 和 aux 之间传输此类状态数据可能会有很大的开销进程,并且可能需要做一些工作来使状态 itrms 可交付(有些是复杂的树结构,可能会因运行脚本而改变)。
【解决方案2】:

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

当用户进程完成时,您可以杀死工作人员。但是如果 worker 被触发,你会得到一个键盘中断的等价物,所以你可以捕捉到产生的异常并进行清理。

这样你在与主程序共享你的东西时不会出现内存问题,但你仍然会在一段固定的时间后被戳到,从而杀死失控的子程序。

【讨论】:

  • 感谢您的回答。不幸的是,主线程运行 GUI,所以我不能在那里运行脚本(如果脚本确实需要超过一秒钟的时间,这可能会使 GUI 令人沮丧,因为在某些情况下,脚本每秒运行多次) .看来 SIGINT 和 co 只能在主线程中触发,因此不会中断其他线程,因此即使 GUI 被“通知”有问题,GUI 也无能为力。还是我从您的回答中遗漏了什么?
  • 不,我只是想建议您翻转选项 #3 中建议的 fork,以保持更多命名空间可访问,因为这似乎是一个问题。我没有意识到您的脚本如此普遍。
【解决方案3】:

尽管选项 1 通常被贬低,但选项 2 或 4 和 1 会很好地结合起来。

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

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

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

我想这里的部分重点是您可能希望不直接使用exec 执行用户的脚本,而是通过将模块加载到您exec 的东西中,这样您就可以在它周围打包 Python 代码,包括尝试/except 块,并安装诸如 settrace 或 Timer 可以注入线索的线程等。

【讨论】:

    【解决方案4】:

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

    选项#3 是最好的(与#4 半结合)。这就是单独流程的用途——单独工作,同时让第一个流程继续处理其他事情。设置起来并不难。

    新进程 (P2) 不必包含对 GUI 中的对象的调用,应该分离关注点。这并不意味着他们不能互动。使用套接字对进行通信,例如,双方都使用pickle 来相互发送 python 对象。 GUI 主循环应该安排一个检查,例如50 毫秒,检查它的套接字,该套接字设置为非阻塞,用于来自 P2 的通信。然后 P2 可以根据需要向 GUI 发送消息,GUI 会对此做出响应。

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

    下面的代码只是部分 sn-ps,根本没有经过测试,只是为了让您了解架构。最好分成不同的部分,例如一个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 可以随时使用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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-30
      • 1970-01-01
      相关资源
      最近更新 更多