在Python中处理阻塞函数调用

5 投票
8 回答
14396 浏览
提问于 2025-04-16 06:17

我正在使用Gnuradio框架。我处理生成的流程图来发送和接收信号。这些流程图可以初始化并启动,但它们不会把控制权返回给我的应用程序:

我导入了time

while time.time() < endtime:
        # invoke GRC flowgraph for 1st sequence
        if not seq1_sent:
            tb = send_seq_2.top_block()
            tb.Run(True)
            seq1_sent = True
            if time.time() < endtime:
                break

        # invoke GRC flowgraph for 2nd sequence
        if not seq2_sent:
            tb = send_seq_2.top_block()
            tb.Run(True)
            seq2_sent = True
            if time.time() < endtime:
                break

问题是:只有第一个if语句调用了与硬件交互的流程图。我对此感到困惑。我可以使用线程,但我对如何在Python中设置线程超时没有经验。我怀疑这是否可能,因为看起来在API中并没有提供杀死线程的功能。这个脚本只需要在Linux上运行...

你是如何正确处理Python中的阻塞函数的 - 不用杀掉整个程序。另一个更具体的例子是:

import signal, os

def handler(signum, frame):
        # print 'Signal handler called with signal', signum
        #raise IOError("Couldn't open device!")
        import time
        print "wait"
        time.sleep(3)


def foo():
    # Set the signal handler and a 5-second alarm
    signal.signal(signal.SIGALRM, handler)
    signal.alarm(3)

    # This open() may hang indefinitely
    fd = os.open('/dev/ttys0', os.O_RDWR)
    signal.alarm(0)          # Disable the alarm


foo()
print "hallo"

我怎么才能仍然看到print "hallo"的输出呢?;)

谢谢,
Marius

8 个回答

1

你可以设置一个信号警报,这样在通话超时的时候就会中断你的通话:

http://docs.python.org/library/signal.html

signal.alarm(1) # 1 second

my_blocking_call()
signal.alarm(0)

如果你想确保这个信号不会破坏你的应用程序,你还可以设置一个信号处理器:

def my_handler(signum, frame):
    pass

signal.signal(signal.SIGALRM, my_handler)

编辑: 这段代码有什么问题呢?它不应该让你的应用程序崩溃:

import signal, time

def handler(signum, frame):
    print "Timed-out"

def foo():
    # Set the signal handler and a 5-second alarm
    signal.signal(signal.SIGALRM, handler)
    signal.alarm(3)

    # This open() may hang indefinitely
    time.sleep(5)
    signal.alarm(0)          # Disable the alarm


foo()
print "hallo"

事情是这样的:

  1. SIGALRM的默认处理方式是让应用程序崩溃,如果你设置了自己的处理器,那么它就不会再停止应用程序了。

  2. 接收到信号通常会中断系统调用(然后让你的应用程序恢复运行)

4

如果我理解得没错,每个顶层块(top_block)都有一个停止的方法。所以你其实可以在一个线程里运行这个顶层块,并在超时到达时发出停止的指令。如果顶层块的wait()方法也能设置超时,那就更好了,但可惜的是,它没有这个功能。

在主线程中,你需要等待两种情况:a) 顶层块完成,b) 超时到期。一直忙着等待是个坏主意,所以你应该使用线程的带超时的join方法来等待这个线程。如果在等待结束后线程还在运行,你就需要停止这个顶层块的运行。

6

首先,尽量避免使用信号:

1) 这可能会导致死锁。比如说,SIGALRM信号可能在阻塞的系统调用之前就到达了进程(想象一下系统负载非常高的情况!),而这个系统调用不会被打断。这样就会出现死锁。

2) 操作信号可能会引发一些意想不到的问题。例如,其他线程中的系统调用可能会被打断,这通常不是你想要的。正常情况下,当接收到一个非致命的信号时,系统调用会被重新启动。但是,当你设置了信号处理程序时,这种行为会自动关闭,影响整个进程或线程组。你可以查看一下'man siginterrupt'来了解更多。

相信我,我之前遇到过两个这样的麻烦,真的不好处理。

在某些情况下,可以明确避免阻塞。我强烈建议使用select()及其相关函数(可以查查Python中的select模块)来处理阻塞的读写操作。不过,这并不能解决阻塞的open()调用。

为此,我测试了这个解决方案,它在命名管道上效果很好。它以非阻塞的方式打开管道,然后关闭这个模式,使用select()调用来最终超时,如果没有数据可用的话。

import sys, os, select, fcntl

f = os.open(sys.argv[1], os.O_RDONLY | os.O_NONBLOCK)

flags = fcntl.fcntl(f, fcntl.F_GETFL, 0)
fcntl.fcntl(f, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)

r, w, e = select.select([f], [], [], 2.0)

if r == [f]:
    print 'ready'
    print os.read(f, 100)
else:
    print 'unready'

os.close(f)

可以用以下方式测试:

mkfifo /tmp/fifo
python <code_above.py> /tmp/fifo (1st terminal)
echo abcd > /tmp/fifo (2nd terminal)

通过一些额外的努力,select()调用可以作为整个程序的主循环,聚合所有事件——你可以使用libev或libevent,或者它们的Python封装。

当你无法明确强制非阻塞行为,比如说你只是使用一个外部库时,这会变得更加困难。虽然线程可以做到,但显然这并不是一种先进的解决方案,通常来说是错误的。

我担心一般来说你无法以稳健的方式解决这个问题——这真的取决于你阻塞的是什么。

撰写回答