Python 包含线程的生成器中似乎未处理的异常

Python 包含线程的生成器中似乎未处理的异常,python,multithreading,exception,exception-handling,generator,Python,Multithreading,Exception,Exception Handling,Generator,考虑下面的代码 其目的是要有一个管理线程的生成器。线程负责寻找数据源,将数据推送到大小为1的fifo上,生成器从中弹出并产生数据 它正常运行,打印出一个不断增加的整数,与close线程集交错?错误 现在,我希望始终调用运行在foo主线程中的块,例如使用ctrl-c,在这种情况下,将设置close\u thread,线程将退出 不幸的是,这似乎并没有可靠地发生,当使用ctrl-c中断时,主线程停止,但收单机构线程继续(打印出close thread set?False,直到我杀死它)。我想知道这个

考虑下面的代码

其目的是要有一个管理线程的生成器。线程负责寻找数据源,将数据推送到大小为1的fifo上,生成器从中弹出并产生数据

它正常运行,打印出一个不断增加的整数,与
close线程集交错?错误

现在,我希望始终调用运行在
foo
主线程中的
块,例如使用
ctrl-c
,在这种情况下,将设置
close\u thread
,线程将退出

不幸的是,这似乎并没有可靠地发生,当使用
ctrl-c
中断时,主线程停止,但收单机构线程继续(打印出
close thread set?False
,直到我杀死它)。我想知道这个异常是否没有传递给包装器对象中的生成器

我错过了什么明显的东西吗

from threading import Thread, Event
from collections import deque
from time import sleep

def foo():

    # As used, deque is thread safe
    processing_fifo = deque([], maxlen=1)
    close_thread = Event()
    close_thread.clear()

    def acquirer():
        a = 0

        try:
            while True:
                processing_fifo.append(a)
                a += 1

                print 'close thread set?', close_thread.is_set()                
                if close_thread.is_set():
                    break

                sleep(1e-6)

        finally: # Make sure we capture any exceptions in the thread
            close_thread.set()

    try:
        acquirer_thread = Thread(target=acquirer)
        acquirer_thread.start()

        while True:
            try:
                yield processing_fifo.popleft()

            except IndexError:
                if close_thread.is_set():
                    break

            sleep(1e-6)

    finally:
        close_thread.set()
        acquirer_thread.join()


class Bleh(object):

    def __init__(self):

        self.gen = foo()

    def eep(self):

        return self.gen.next()

obj = Bleh()

while True:
    print obj.eep()
编辑
将线程设置为守护进程线程似乎就足够了(
acquisitor\u thread.daemon=True
)。在这种情况下,子线程中的
finally:
块将可靠地执行。不过,这并没有让我更进一步地理解这个问题。

当您按下
ctrl-c
时,它只中断主线程,主线程退出,但让Python进程运行,因为您生成的线程一直在运行-它们不会收到
键盘中断

因此,如果不将线程设置为
daemon=true
,程序将不会退出:

线程可以标记为“守护线程”。此标志的意义在于,当只剩下守护进程线程时,整个Python程序将退出。初始值从创建线程继承。可以通过daemon属性设置该标志

在这种情况下,当您的线程被设置为守护进程时,当您的程序执行
ctrl-c
时,它们也将被强制退出,从而导致执行
except

尽管可以将线程设置为
daemon=True
,但还有另一种方法。尝试以下方法:

keep_running = True
def acquirer():
    try:
        while keep_running:
            ...
然后在主线程中捕获
键盘中断
异常:

try:
    ...
except KeyboardInterrupt:
    # thread will see this change on next iteration and exit
    keep_running = False 

问题是,当主线程不在调用
obj.eep
的内部时,有时会发生
键盘中断。在Python中,信号(如执行Ctrl+C操作时引发的SIGINT):

[O] 只有主线程可以设置新的信号处理程序,主线程 将是唯一接收信号的设备(这由 Python信号模块,即使底层线程实现 支持向单个线程发送信号)

如果SIGINT发生在
obj.eep
之外,则不会发生异常处理,因此不会设置事件对象。如果您在每次调用
obj.eep()
后都添加一个短暂的睡眠,则可以使这种情况更加一致

如果将事件对象设置为全局,并在True:obj.eep()
时在
周围添加一个
try
/
finally
,则问题会消失:

from threading import Thread, Event
from collections import deque
from time import sleep

def foo():
    # As used, deque is thread safe
    processing_fifo = deque([], maxlen=1)
    #close_thread = Event()
    close_thread.clear()

    def acquirer():
        a = 0

        try:
            while True:
                processing_fifo.append(a)
                a += 1

                print 'close thread set?', close_thread.is_set()                
                if close_thread.is_set():
                    break

                sleep(1e-6)

        finally: # Make sure we capture any exceptions in the thread
            close_thread.set()

    try:
        acquirer_thread = Thread(target=acquirer)
        acquirer_thread.start()

        while True:
            try:
                yield processing_fifo.popleft()

            except IndexError:
                if close_thread.is_set():
                    break

            sleep(1e-6)

    finally:
        close_thread.set()
        acquirer_thread.join()


class Bleh(object):

    def __init__(self):

        self.gen = foo()

    def eep(self):

        return self.gen.next()

close_thread = Event()
obj = Bleh()

try:
    while True:
        print obj.eep()
finally:
    close_thread.set()

是的,在主进程存在时杀死子线程的正确方法是将它们设置为守护进程。但是请记住,守护进程线程不能被加入,因此您可以删除该加入。为什么生成器不能接收异常?我是否遗漏了在生成器内部进行异常处理的含义?根据我粗略的了解,close方法应该确保调用了
finally
。@HenryGomersall如果在主线程位于生成器内部时出现Ctrl+C,它将始终被
try
/
finally
捕获;这就是为什么Ctrl+C有时有效。但是,如果主线程在调用
obj.eep()
的外部(因此不在生成器内部),那么
键盘中断不会被任何东西捕获。添加额外的
try/
finally`可以确保主线程始终受到保护。但是
close
意味着在清理生成器对象时在生成器上调用,这将触发
finally
子句。这只是没有发生吗?对,这是问题的关键:
obj
有时不会被删除,因此生成器不会被删除。将
delobj
放在与上面代码中最后一个
finally
相同的位置就足够了。当python解释器在退出时没有正确清理时,这是一个以各种形式显示的问题。@HenryGomersall看起来生成器对象没有清理,因为线程对象没有清理-我猜python解释器试图等待运行线程退出,然后清理剩余的obejcts,并最终阻塞等待线程完成,从而阻止它清理生成器对象。