Ubuntu에서 multiprocessing.Process 상속받은 클래스는 동작하는데 윈도우가면 안됨

Table of Content

사건의 개요

RTSP 영상 수신 프로세스를 만들고 있었다. 이 프로세스는 ffmpeg 라이브러리의 wrapper인 PyAv라는 모듈로 영상을 수신하여 numpy이미지로 읽어주는 간단한 코드이다.

# 별도의 프로세스로 동작하면서 영상을 받아오는 모듈. 오버라이딩 함수인 run함수는 편의상 쓰지 않았다
class IPCamStreamer(multiprocessing.Process):
    r"""
    IPCamStreamer : Process which read video stream via rtsp protocol and Write frame on shared memory
    """
    def __init__(self, url: str, streamerName: str):
        super(IPCamStreamer, self).__init__()
        self.url = url
        self.name = streamerName

        self.daemon = True

        self.videoReader = VideoReader(self.url)

    def run(self) -> None:
        super(IPCamStreamer, self).run()
# PyAv를 이용하여 비디오를 읽어주는 클래스. 일부만 발췌하였다.
class VideoReader:
    r"""
    RTSP, Video FILE 등 동영상을 인코딩하여 numpy.ndarray 또는 PIL 이미지로 사용하게 해주는 클래스 입니다.
    """
    def __init__(self, path: str = ""):
        r"""
        :param str path: Path for open video. Ex. "/dev/video0", "rtsp://<ip>/:8554", "./video/some_video.mp4"
        """
        self.url = path

        self.containerOptions = {
            "buffer_size": "256000",
            "stimeout": "20000000",
            "max_delay": "3000000",
            "discard_corrupt": "DISCARD_CORRUPT",
            "gen_pts": "GENPTS",
            "non_block": "NONBLOCK",
            "stimeout": "20000000",
            "max_delay": "3000000"
        }

        if "rtsp" in path:
            self.containerOptions["rtsp_transport"] = "udp"

        self.container = None
        self._frameNumber = -1

        self.container = av.open(path, container_options=self.containerOptions, timeout=(1.0, 0.1), format=None)

위 코드는 Ubuntu에서는 정상 동작하고 윈도우에서는 아래의 에러를 내며 동작하지 않는다.

C:\Users\dspar\PycharmProjects\kisa_rtsp_reciver\venv\Scripts\python.exe C:/Users/dspar/Desktop/Project/human-activity-understanding/ipcam_stream_client.py target_ip.txt
Traceback (most recent call last):
  File "C:/Users/dspar/Desktop/Project/human-activity-understanding/ipcam_stream_client.py", line 109, in <module>
    process.start()
  File "C:\Users\dspar\AppData\Local\Programs\Python\Python38\lib\multiprocessing\process.py", line 121, in start
    self._popen = self._Popen(self)
  File "C:\Users\dspar\AppData\Local\Programs\Python\Python38\lib\multiprocessing\context.py", line 224, in _Popen
    return _default_context.get_context().Process._Popen(process_obj)
  File "C:\Users\dspar\AppData\Local\Programs\Python\Python38\lib\multiprocessing\context.py", line 327, in _Popen
    return Popen(process_obj)
  File "C:\Users\dspar\AppData\Local\Programs\Python\Python38\lib\multiprocessing\popen_spawn_win32.py", line 93, in __init__
    reduction.dump(process_obj, to_child)
  File "C:\Users\dspar\AppData\Local\Programs\Python\Python38\lib\multiprocessing\reduction.py", line 60, in dump
    ForkingPickler(file, protocol).dump(obj)
  File "stringsource", line 2, in av.container.input.InputContainer.__reduce_cython__
TypeError: no default __reduce__ due to non-trivial __cinit__

Process finished with exit code 1

윈도우에서 동작하게 만들기 위해서는 단순히 VideoReader 클래스의 open 함수를 run 함수 내부에서 쓰기만 하면 된다.

위의 에러메시지 중 마지막 트레이스 백을 보면
ForkingPickler(file, protocol).dump(obj) 라는 메시지가 나타난다.

파이썬에서 multiprocess.Process의 __init__() 함수 안에서 호출되는 모든 함수들과 변수들은 직렬화가 가능해야한다. 특히나 multiprocess.Process의 서브 클래스를 만들고자 하는 경우 multiprocess.Process.start() 메서드가 불리기 전에 서브클래스의 인스턴스는 반드시 직렬화가 가능해야 한다.

init 함수 안의 av.open함수가 리턴하는 input_container가 아마도 직렬화가 되지 않기 때문에 VideoReader가 생성되고 나서 run함수에 들어가게 되면 문제가 발생하는 것으로 보인다.

정말로 직렬화의 문제인지 알아보기 위해 직접 pickle을 해보았다.
아래 그림에서 볼 수 있듯 container는 pickable 하지 않다.

av.open함수가 리턴하는 container 객체는 picklable 하지 않다.

아무래도 PyAv는 C++ 라이브러리인 ffmpeg의 라이브러일 뿐이라 C++ 라이브러리 자체에서 직렬화가 가능하도록 코딩이 되어있지 않으면 직렬화가 되지 않을 것이다. 순수 파이썬 원시 타입을 이용해서 만들어진 데이터 타입도 아닐 것이다.

그런데 왜 우분투에서는 되고 윈도우에서는 실행이 안될까?
우분투에서는 Process의 init함수 내에서 직렬화 불가능한 객체를 허용해주는 것일까? 우분투에서만 직렬화가 되있도록 로우 레벨에 구현이 되어있을까?

먼저 우분투에서만 직렬화 되게 구현을 한 것은 아니다. 우분투 파이썬에서 똑같이 돌려보았을 때 여전이 unpicklable 하였다. 그도 당연한 것이 크로스 플랫폼을 위해 중간 언어를 쓰는 인터프리터가 OS에 따라 객체 직렬화 가능여부를 달리한다면 이는 시스템이 일관적이지 못한 것이다.

우분투와 윈도우에서 multiprocessing을 할 때 가장 큰 차이는 프로세스 생성 방식이 fork이냐 spawn이냐 이다. 이 생성 방식이 원인을 찾는 실마리가 될 것 같다.
우분투에서 메인 함수에 multiprocessing.set_startmethod("spawn")으로 할 경우 우분투에서도 같은 에러 메시지를 볼 수 있다.

에러 트레이스 백을 보면 아래 모듈에 작성된 forkingPickler 클래스가 보인다.
PyAv는 ffmpeg의 C++의 wrapper를 만들기 위해 cpython을 사용하였을 것이다. forkingPickler의 코드를 보다보면 _winapi 일 때 분기를 해놓은 것을 볼 수있다.
cpython/reduction.py at master · python/cpython · GitHub

결론을 내리자면, multiprocessing.Process는 모든 것이 직렬화 되어야 하는데 cpython으로 ffmpeg을 wrapping한 PyAv는 cpython이 윈도우 일 경우 c++ 클래스의 직렬화 구현이 되어있지 않아 생기는 문제라고 생각한다.

Related Posts

답글 남기기

이메일 주소는 공개되지 않습니다.