Multiprocessing Pipe send() blocks

岁酱吖の 提交于 2019-12-24 08:25:39

问题


According to Python documentation, only recv() blocks but not send(). I wrote the following code trying to make a GUI sudoku game. I made it in such a way that I can update the game board even if the tkinter is executing its mainloop. However, during a test run, I found that if I close the window while the game is updating, the pipe.send() starts to block (I found that out using CPython profiler.) Can anyone please tell me why and, if possible, how to fix this issue?

To produce the issue: close the poped up window while the script is updating. That is, close the window while some numbers are printed to console.

My system: macOS Sierra 10.12.5

import multiprocessing as mp
import threading
import random
import time
try:
    import tkinter as tk  # Python3
except ImportError:
    import Tkinter as tk  # Python2

class VisualizedBoard:
    def __init__(self,input_string,pipe):
        '''input_string: a string has a length of at least 81 that represent the board from top-left to bottom right.
        empty cell is 0'''

        self.update_scheduled=False
        self.pipe=pipe

        # create board
        self.root = tk.Tk()
        self.canvas = tk.Canvas(self.root, width=500, height=500)
        self.canvas.create_rectangle(50, 50, 500, 500, width=2)
        for i in range(1, 10):
            self.canvas.create_text(25 + 50 * i, 30, text=str(i))
            self.canvas.create_text(30, 25 + 50 * i, text=str(i))
            self.canvas.create_line(50 + 50 * i, 50, 50 + 50 * i, 500, width=2 if i % 3 == 0 else 1)
            self.canvas.create_line(50, 50 + 50 * i, 500, 50 + 50 * i, width=2 if i % 3 == 0 else 1)

        for i in range(81):
            if input_string[i] != '0':
                self.canvas.create_text(75 + 50 * (i // 9), 75 + 50 * (i % 9), tags=str((i//9+1,i%9+1)).replace(' ',''),text=input_string[i], fill='black')

        self.canvas.pack()
        self.root.attributes('-topmost', True)

        self.root.geometry('550x550+%d+%d' % ((self.root.winfo_screenwidth() - 550) // 2, (self.root.winfo_screenheight() - 550) // 2))
        self.root.wm_protocol('WM_DELETE_WINDOW',lambda :(self.root.destroy()))
        threading.Thread(target=self.listen, args=()).start()
        self.root.mainloop()


    def update(self,coordinate,value,color='magenta'):
        """
                :parameter coordinate: a tuple (x,y)
                :parameter value: single digit
                :returns: None
        """
        try:
            assert isinstance(coordinate,tuple)
        except AssertionError:
            print('Update Failed. Coordinate should be a tuple (x,y)')

        coordinate_tag=str(coordinate).replace(' ','')
        self.canvas.delete(coordinate_tag)
        if value != 0 and value != '0':
            self.canvas.create_text(25+50*coordinate[0],25+50*coordinate[1],tags=coordinate_tag,text=str(value),fill=color)

        self.postponed_update()

        #self.canvas.update()

    def postponed_update(self):
        if not self.update_scheduled:
            self.canvas.after(50,self.scheduled_update)
            self.update_scheduled=True

    def scheduled_update(self):
        self.canvas.update()
        self.update_scheduled=False

    def new_board(self,input_string):
        self.root.destroy()
        return VisualizedBoard(input_string,self.pipe)

    def listen(self):
        try:
            while True:
                msg=self.pipe.recv()
                self.update(*msg)
        except EOFError:
            self.pipe.close()
            tk.Label(self.root,text='Connection to the main script has been closed.\nIt is safe to close this window now.').pack()
        except Exception as m:
            self.pipe.close()
            print('Error during listing:',m)



class BoardConnection:
    def __init__(self,input_string):
        ctx = mp.get_context('spawn')
        self.receive,self.pipe=ctx.Pipe(False)
        self.process=ctx.Process(target=VisualizedBoard,args=(input_string,self.receive))
        self.process.start()

    def update(self,coordinate,value,color='magenta'):
        """
        :parameter coordinate: a tuple (x,y)
        :parameter value: single digit
        :returns: None
        """
        self.pipe.send((coordinate,value,color))

    def close(self):
        self.pipe.close()
        self.process.terminate()





if __name__ == "__main__":
    b=BoardConnection('000000000302540000050301070000000004409006005023054790000000050700810000080060009')
    start = time.time()
    for i in range(5000): #test updating using random numbers
        b.update((random.randint(1, 9), random.randint(1, 9)), random.randrange(10))
        print(i)
    print(time.time() - start)

回答1:


Python Pipe is an abstractions on top of OS nameless pipes.

An OS pipe is generally implemented as a memory buffer of a certain size within the kernel. By default, if the buffer fills up the next call to send/write will block.

If you want to be able to continue publishing data even if no consumer is consuming it, you should either use a multiprocessing.Queue or asyncio facilities.

The multiprocessing.Queue employs a "limitless" buffer and a thread to push the data into the OS pipe. If the pipe gets full the caller will continue running as the published data will be piled up in the Queue buffer.

IIRC, asyncio sets the pipe O_NONBLOCK flag and waits for the pipe to be consumed. Additional messages are stored within a "limitless" buffer as for the multiprocessing.Queue.



来源:https://stackoverflow.com/questions/44916079/multiprocessing-pipe-send-blocks

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!