完成当前迭代后,如何结束内部套接字操作的无限循环?

How can I end an infinite loop with socket operations inside after finishing current iteration?

我有一个无限循环,其中有一些操作在退出循环之前必须完全执行。也就是说,我正在使用套接字库连接到外部设备,我需要等待读取指令完成才能中断循环。

我尝试使用信号处理程序(如 )在检测到键盘中断时升起标志。

当前代码:

import videosensor
import signal

def signal_handler(signal, frame):
    """Raises a flag when a keyboard interrupt is raised."""
    global interrupted
    interrupted = True

if __name__ == '__main__':
    camera = videosensor.VideoSensor(filename)
    interrupted = False
    signal.signal(signal.SIGINT, signal_handler)

    while not interrupted:
        location = camera.get_register()
        #...
        #More irrelevant stuff is executed.
        #...
        time.sleep(0.01)

    #This code has to be executed after exiting while loop
    camera_shutdown(camera)

在前面的代码中,videosensor.VideoSensor是一个class,包含从外部设备获取数据的套接字操作。主程序中使用的 get_register() 方法如下:

def get_register(self):
    """Read the content of the specified register.
    """
    #Do some stuff
    value = socket.recv(2048)
    return value

问题:

我希望 while 循环持续执行,直到用户按下某个键或使用键盘中断,但在当前迭代完成之后。相反,使用以前的解决方案并不能按预期工作,因为它会中断正在进行的指令,并且如果它正在读取套接字,则会引发错误:

/home/.../client.pyc in read_register(self, regkey)

    164         reg = self._REGISTERS[regkey]
    165         self.send('r,{}\n'.format(reg))
--> 166         value = socket.recv(2048)
    167         #Convert the string input into a valid value e.g. list or int
    168         formatted_result = ast.literal_eval(value)

error: [Errno 4] Interrupted system


编辑: 从下面的答案看来,似乎无法使用 Keyboard Interrupt 并避免套接字读取要中止的功能。尽管有捕获错误的解决方案,但它们无法避免读取取消。

不过,我有兴趣找到一种获取用户输入的方法,例如特定的按键按下,这会引发标志,将在循环结束时检查,在检查之前不会中断主例程的执行。

EDIT2: 使用的 OS 是 Linux 分布 Ubuntu 14.04

您创建了自定义信号处理程序,但没有覆盖默认的键盘中断行为。将 signal.signal(signal.SIGINT, signal_handler) 添加到您的代码中以完成此操作:

import videosensor
import signal

# Custom signal handler
def signal_handler(signal, frame):
    """Raises a flag when a keyboard interrupt is raised."""
    global interrupted
    interrupted = True

# Necessary to override default keyboard interrupt
signal.signal(signal.SIGINT, signal_handler)  

if __name__ == '__main__':
    # Main programme

快速搜索后我发现 this solution for your issue

基本上,您无能为力:当您向进程发送 SIGINT 时,套接字也会 return SIGINT。那么,您能做的最好的事情就是通过捕获套接字 EINTR 错误并继续循环来主动忽略该问题:

import errno

try:
    # do something
    value = conn.recv(2048)
except socket.error as (code, msg):
    if code != errno.EINTR:
        raise

避免 C-c 中断读取问题的替代解决方案是使用并行执行,在例程中读取套接字,并在另一个例程中处理用户输入:

import asyncio

async def camera_task(has_ended, filename):
    camera = videosensor.VideoSensor(filename)

    try:
        while not has_ended.is_set():
            location = camera.get_register()
            #...
            #More irrelevant stuff is executed.
            #...
            await asyncio.sleep(0.01)
    finally:
        #This code has to be executed after exiting while loop
        camera_shutdown(camera)

async def input_task(shall_end):
    while True:
        i = input("Press 'q' to stop the script…")
        if i == 'q':
            shall_end.set()

def main():
    filename = …
    #
    end_event = asyncio.Event()
    asyncio.Task(camera_task(end_event, filename))
    asyncio.Task(input_task(end_event))
    asyncio.get_event_loop().run_forever()

threading

import threading, time

def camera_task(has_ended, filename):
    camera = videosensor.VideoSensor(filename)

    try:
        while not has_ended.is_set():
            location = camera.get_register()
            #...
            #More irrelevant stuff is executed.
            #...
            time.sleep(0.01)
    finally:
        #This code has to be executed after exiting while loop
        camera_shutdown(camera)

def input_task(shall_end):
    while True:
        i = input("Press 'q' to stop the script…")
        if i == 'q':
            shall_end.set()

def main():
    filename = …
    #
    end_event = threading.Event()
    threads = [
        threading.Thread(target=camera_task, args=(end_event, filename)),
        threading.Thread(target=input_task, args=(end_event,))
    ]
    # start threads
    for thread in threads:
        thread.start()
    # wait for them to end
    for thread in threads:
        thread.join()

multiprocessing:

import multiprocessing, time

def camera_task(has_ended, filename):
    camera = videosensor.VideoSensor(filename)

    try:
        while not has_ended.is_set():
            location = camera.get_register()
            #...
            #More irrelevant stuff is executed.
            #...
            time.sleep(0.01)
    finally:
        #This code has to be executed after exiting while loop
        camera_shutdown(camera)

def input_task(shall_end):
    while True:
        i = input("Press 'q' to stop the script…")
        if i == 'q':
            shall_end.set()

def main():
    filename = …
    #
    end_event = multiprocessing.Event()
    processes = [
        multiprocessing.Process(target=camera_task, args=(end_event, filename)),
        multiprocessing.Process(target=input_task, args=(end_event,))
    ]
    # start processes
    for process in processes:
        process.start()
    # wait for them to end
    for process in processes:
        process.join()

disclaimer: those codes are untested, and there might be some typos or little errors, but I believe the overall logic should be

如果我理解正确,您不希望 socket.recv() 被打断,但您确实希望使用信号让用户指示 I/O 循环应该在当前 I/O 操作已完成。

假设你在Unix系统上使用Python2,你可以通过在进入循环前调用signal.siginterrupt(signal.SIGINT, False)来解决你的问题。这将导致系统调用在信号发生时重新启动,而不是中断它并引发异常。

在您的情况下,这意味着 socket.recv() 操作将在您的信号处理程序被调用后重新启动,因此 get_register() 不会 return 直到在套接字上收到消息。如果这是您想要的,您的代码将是:

    interrupted = False
    old_handler = signal.signal(signal.SIGINT, signal_handler)    # install signal handler
    signal.siginterrupt(signal.SIGINT, False)                     # do not interrupt system calls

    while not interrupted:
        location = camera.get_register()
        if location == '':
            # remote connection closed
            break
        #...
        #More irrelevant stuff is executed.
        #...
        time.sleep(0.01)

这是一种方法,但它确实要求您的代码在 Unix 平台上 运行。

另一种可能适用于其他平台的方法是处理异常,忽略进一步的 SIGINT 信号(以防用户再次点击中断),然后在之前执行最后的 socket.recv() return来自 get_register() 函数:

import errno

def get_register(s):
    """Read the content of the specified register.
    """
    #Do some stuff
    try:
        old_handler = None
        return s.recv(2048)
    except socket.error as exc:
        if exc.errno == errno.EINTR:
            old_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)    # ignore this signal
            return s.recv(2048)    # system call was interrupted, restart it
        else:
            raise
    finally:
        if old_handler is not None:
            signal.signal(signal.SIGINT, old_handler)    # restore handler

信号处理可能会很棘手,上面可能存在我不知道的竞争条件。如果可能,请尝试使用 siginterrupt()