Python通过对命名管道的非阻塞写入来避免部分写入

问题描述

我正在linux上运行python3.8。

在我的脚本中,创建一个命名管道,并按如下所示打开它:

import os
import posix
import time

file_name = 'fifo.txt'
os.mkfifo(file_name)

f = posix.open(file_name,os.O_RDWR | os.O_NONBLOCK)
os.set_blocking(f,False)

在尚未打开文件以供其他地方读取的情况下(例如,使用cat),我开始循环写入文件


base_line = 'abcdefghijklmnopqrstuvwxyz'
s = base_line * 10000 + '\n'

while True:
    try:
        posix.write(f,s.encode())
    except BlockingIOError as e:
        print("Exception occurred: {}".format(e))
    time.sleep(.5)

然后我使用cat从命名管道中读取时,发现发生了部分写入。

我很困惑如何知道在此实例中写入了多少字节。由于引发了异常,因此我无权访问返回值(写入的字节数)。该文档建议BlockingIOError具有一个名为characters_written属性,但是当我尝试访问该字段时,会引发AttributeError

总而言之:如何首先避免这种部分写入,或者至少知道在这种情况下部分写入了多少?

解决方法

os.write执行无缓冲写入。文档指出,BlockingIOError仅在缓冲的写操作将阻塞时才具有characters_written属性。

如果在管道变满之前成功写入了任何字节,那么将从os.write返回该字节数。否则,您将获得一个例外。当然,即使写入了一些字节,诸如驱动器故障之类的事情也会导致异常。这与POSIX write的工作方式没有什么不同,除了引发错误以外,不是返回-1来返回错误。

如果您不喜欢处理异常,则可以在文件描述符周围使用包装器,例如io.FileIO对象。我已经修改了您的代码,因为它会在您每次循环回到os.write调用时尝试写入整个缓冲区(如果一次失败,则每次都会失败):

import io
import os
import time

base_line = 'abcdefghijklmnopqrstuvwxyz'
data = (base_line * 10000 + '\n').encode()

file_name = 'fifo.txt'
os.mkfifo(file_name)
fd = os.open(file_name,os.O_RDWR | os.O_NONBLOCK)
# os.O_NONBLOCK makes os.set_blocking(fd,False) unnecessary.

with io.FileIO(fd,'wb') as f:
    written = 0
    while written < len(data):
        n = f.write(data[written:])
        if n is None:
            time.sleep(.5)
        else:
            written += n

顺便说一句,您可以使用selectors模块而不是time.sleep;我注意到尝试从管道读取数据时会出现一些延迟,因为存在睡眠延迟,如果您使用selectors模块,则不会发生这种延迟:

with io.FileIO(fd,'wb') as f:
    written = 0
    sel = selectors.DefaultSelector()
    sel.register(f,selectors.EVENT_WRITE)
    while written < len(data):
        n = f.write(data[written:])
        if n is None:
            # Wait here until we can start writing again.
            sel.select()
        else:
            written += n
    sel.unregister(f)

POSIX named pipe (fifo) drops record in nonblocking mode的答案中也可以找到一些有用的信息。