通过管道进行python进程通信:竞争条件

时间:2012-10-29 15:04:12

标签: python named-pipes race-condition python-3.2

所以我有两个需要相互通信的Python3.2进程。需要传达的大多数信息都是标准词典。命名管道似乎是要走的路,所以我创建了一个可以在两个进程中实例化的管道类。这个类实现了一个非常基本的协议来获取信息。

我的问题是,有时候它会起作用,有时则不然。除了代码失败的地方外,似乎没有这种行为的模式。

以下是重要的Pipe类的位。如果您想要更多代码,请大声喊叫:

class Pipe:
    """
    there are a bunch of constants set up here. I dont think it would be useful to include them. Just think like this: Pipe.WHATEVER = 'WHATEVER'
    """
    def __init__(self,sPath):
        """
        create the fifo. if it already exists just associate with it
        """
        self.sPath = sPath
        if not os.path.exists(sPath):
            os.mkfifo(sPath)
        self.iFH = os.open(sPath,os.O_RDWR | os.O_NONBLOCK)
        self.iFHBlocking = os.open(sPath,os.O_RDWR)

    def write(self,dMessage):
        """
        write the dict to the fifo
        if dMessage is not a dictionary then there will be an exception here.  There never is 
        """
        self.writeln(Pipe.MESSAGE_START)
        for k in dMessage:
            self.writeln(Pipe.KEY)
            self.writeln(k)
            self.writeln(Pipe.VALUE)
            self.writeln(dMessage[k])
        self.writeln(Pipe.MESSAGE_END)

    def writeln(self,s):
        os.write(self.iFH,bytes('{0} : {1}\n'.format(Pipe.LINE_START,len(s)+1),'utf-8'))
        os.write(self.iFH,bytes('{0}\n'.format(s), 'utf-8'))
        os.write(self.iFH,bytes(Pipe.LINE_END+'\n','utf-8'))

    def readln(self):
        """
        look for LINE_START, get line size
        read until LINE_END
        clean up
        return string
        """
        iLineStartBaseLength = len(self.LINE_START)+3  #'{0} : '
        try:
            s = os.read(self.iFH,iLineStartBaseLength).decode('utf-8')
        except:
            return Pipe.READLINE_FAIL

        if Pipe.LINE_START in s:
            #get the length of the line
            sLineLen = ''
            while True:
                try:
                    sCurrent = os.read(self.iFH,1).decode('utf-8')
                except:
                    return Pipe.READLINE_FAIL
                if sCurrent == '\n':
                    break
                sLineLen += sCurrent
            try:
                iLineLen = int(sLineLen.strip(string.punctuation+string.whitespace))
            except:
                raise Exception('Not a valid line length: "{0}"'.format(sLineLen))
            #read the line
            sLine = os.read(self.iFHBlocking,iLineLen).decode('utf-8')

            #read the line terminator
            sTerm = os.read(self.iFH,len(Pipe.LINE_END+'\n')).decode('utf-8')
            if sTerm == Pipe.LINE_END+'\n':
                return sLine
            return Pipe.READLINE_FAIL

        else:
            return Pipe.READLINE_FAIL

    def read(self):
        """
        read from the fifo, make a dict
        """
        dRet        = {}
        sKey        = ''
        sValue      = ''
        sCurrent    = None

        def value_flush():
            nonlocal dRet, sKey, sValue, sCurrent
            if sKey:
                dRet[sKey.strip()] = sValue.strip()
            sKey = ''
            sValue = ''
            sCurrent = ''

        if self.message_start():
            while True:
                sLine = self.readln()
                if Pipe.MESSAGE_END in sLine:
                    value_flush()
                    return dRet
                elif Pipe.KEY in sLine:
                    value_flush()
                    sCurrent = Pipe.KEY
                elif Pipe.VALUE in sLine:
                    sCurrent = Pipe.VALUE
                else:
                    if sCurrent == Pipe.VALUE:
                        sValue += sLine
                    elif sCurrent == Pipe.KEY:
                        sKey += sLine
        else:
            return Pipe.NO_MESSAGE

这里有时会失败(在readln中):

        try:
            iLineLen = int(sLineLen.strip(string.punctuation+string.whitespace))
        except:
            raise Exception('Not a valid line length: "{0}"'.format(sLineLen))

它在任何其他地方都不会失败。

示例错误是:

Not a valid line length: "KE 17"

这是间歇性的事实告诉我,这是由于某种竞争条件,我只是在努力弄清楚它可能是什么。有什么想法吗?

编辑添加了有关调用流程的内容

如何使用Pipe是通过调用具有相同路径的构造函数在processA和ProcessB中实例化的。然后,进程A将间歇性地写入管道,进程B将尝试从中读取。在任何时候我都不会尝试让事情像双向一样。

这是对情况的更长期的解释。我一直在努力保持这个问题的简短,但我认为这是我放弃这个问题的时候了。 Anyhoo,我有一个守护进程和一个金字塔进程,需要玩得很好。正在使用两个Pipe实例:一个只有Pyramid写入,另一个只有守护进程写入。 Pyramid写的东西真的很短,我在这个管道上没有遇到任何错误。守护进程写的东西要长得多,这就是给我带来悲伤的管道。两个管道都以相同的方式实现。这两个进程只将字典写入各自的Pipes(如果不是这种情况,那么Pipe.write中会出现异常)。

基本算法是:金字塔产生守护进程,守护进程加载死亡的狂热对象层次结构和巨大的ram消耗。 Pyramid向守护进程发送POST请求,后者执行一大堆计算并将数据发送到Pyramid,以便可以呈现人性化页面。然后,人类可以通过填写HTML表单等来响应层次结构中的内容,从而使金字塔向守护进程发送另一个字典,守护进程发回字典响应。

所以:只有一个管道出现了任何问题,问题管道的流量比另一个管道多得多,并且只保留字典写入

的保证。

编辑作为对问题和评论的回复

在你告诉我尝试之前......除了阅读的内容。 异常被提出的事实是困扰我的事情。 iLineLengh = int(stuff)在我看来应该总是传递一个看起来像整数的字符串。大多数情况下都是如此,而不是全部。所以如果你觉得有关它可能不是一个整数的评论的冲动请不要。

用我的问题来解释:发现竞争条件,你将成为我的英雄。

编辑一个小例子:

process_1.py:

oP = Pipe(some_path)
while 1:
    oP.write({'a':'foo','b':'bar','c':'erm...','d':'plop!','e':'etc'})

process_2.py:

oP = Pipe(same_path_as_before)
while 1:
    print(oP.read())

2 个答案:

答案 0 :(得分:1)

在玩完代码之后,我怀疑问题来自你如何阅读文件。

具体来说,这样的行:

os.read(self.iFH, iLineStartBaseLength)

该调用不一定返回iLineStartBaseLength个字节 - 它可能会消耗"LI",然后返回READLINE_FAIL并重试。在第二次尝试时,它将获得该行的其余部分,并以某种方式最终将非数字字符串提供给int()调用

不可预测性可能来自于fifo如何被刷新 - 如果在写完整行时碰巧刷新,一切都很好。如果它在行写半写时会刷新,那就很奇怪。

至少在我最终使用的黑客版本的脚本中,oP.read()中的process_2.py电话通常与发送的电话有不同的字典(KEY可能会流入前一个VALUE和其他陌生感。)

我可能会弄错,因为我必须进行一系列更改才能在OS X上运行代码,并进一步进行实验。 My modified code here

不确定如何修复它,但是..使用json模块或类似物,协议/解析可以大大简化 - 换行分离的JSON数据更容易解析:

import os
import time
import json
import errno


def retry_write(*args, **kwargs):
    """Like os.write, but retries until EAGAIN stops appearing
    """

    while True:
        try:
            return os.write(*args, **kwargs)
        except OSError as e:
            if e.errno == errno.EAGAIN:
                time.sleep(0.5)
            else:
                raise


class Pipe(object):
    """FIFO based IPC based on newline-separated JSON
    """

    ENCODING = 'utf-8'

    def __init__(self,sPath):
        self.sPath = sPath
        if not os.path.exists(sPath):
            os.mkfifo(sPath)

        self.fd = os.open(sPath,os.O_RDWR | os.O_NONBLOCK)
        self.file_blocking = open(sPath, "r", encoding=self.ENCODING)

    def write(self, dmsg):
        serialised = json.dumps(dmsg) + "\n"
        dat = bytes(serialised.encode(self.ENCODING))

        # This blocks until data can be read by other process.
        # Can just use os.write and ignore EAGAIN if you want
        # to drop the data
        retry_write(self.fd, dat)

    def read(self):
        serialised = self.file_blocking.readline()
        return json.loads(serialised)

答案 1 :(得分:-1)

尝试删除try:except:块并查看实际抛出的异常。

所以只需用以下代码替换样品:

iLineLen = int(sLineLen.strip(string.punctuation+string.whitespace))

我打赌它现在会抛出ValueError,这是因为你试图将“KE 17”投射到int

如果您要将字符串转换为string.whitespace,则需要删除超过string.punctuationint的内容。