[Concurrency] 멀티프로세싱과 IPC

멀티프로세싱은 OS 프로세스를 단위로 동시성을 구현하는 방법이고, IPC(Inter Process Communication: 프로세스간 통신)은 멀티프로세싱에서 프로세스간 동기화를 달성하는 방법을 말한다.



왜 멀티프로세스인가?

사실 요즘에 들어서는 굳이 멀티스레드를 제쳐두고 멀티프로세스를 고집할 이유가 그리 많지는 않다.
대부분의 경우에는 멀티스레드가 리소스 효율성이 더 낫고, 성능 자체의 고점도 높기 때문이다.

그럼에도 불구하고 멀티프로세싱을 활용하는 경우는 꽤 많다.

1. 그냥 멀티스레드가 지원되지 않는 환경
Python 같은 녀석들은 언어 차원에서 멀티스레드를 지원하지 않는다. 그냥 본인들이 하기 싫어서 미루다가 요즘에 들어서야 넣는다고 하고 있는데, 아무튼 아직은 없다.
그래서 Python에서는 멀티프로세싱을 통해서만 진짜 병렬성을 구현할 수 있다.

2. 안전하고 단순한 모델을 유지하기 위해서
멀티스레딩은 고점 자체는 높지만, 기본적으로 동기화에 드는 노력이나 부작용이 대단히 많다.
그래서 그냥 쉽고 편하게 구조를 만들기 위해서 멀티프로세싱을 선택하는 경우가 꽤 있다.
대표적인 사례가 Erlang이다.
PHP도 비슷하다고 할 수도 있다.




OS 프로세스와 메모리 공유

원래 OS에서 프로세스는 서로 독립된 객체고, 일반적으로는 메모리를 공유하지 못한다.
프로세스에 대한 리소스 제어는 OS가 담당하는 것이다.

그래서 처음에는 좀 변칙적인 패턴들을 통해서 메모리 공유를 달성하곤 했는데, 방법이 꽤 많다.
개중에서도 가장 많이 사용되는 것은 Shared Memory 기법이다.
하나씩 살펴보겠다.




Mail Slot (Windows)

이건 Windows에서만 지원되는 IPC 기법이다.

IPC라고 부르기도 뭐하긴 한데, 그냥 424바이트의 가상공간을 OS에 열어달라 요청하고, 거기다가 프로세스들이 데이터를 읽고 쓰면서 공유하는 것이다.

크기도 제한적이고 연결 자체가 느슨한 방식이라 일반적인 응용프로그램에서는 잘 안 쓴다.




Named Pipe (Unix)

named pipe는 unix 계열 OS에서 지원하는 IPC 기법이다. Windows에도 비슷한게 있긴 한데, 동작이 좀 달라서 호환되지는 않는다.

https://www.scaler.com/topics/linux-named-pipe/
OS에게 pipe라고 하는 파일 단위를 만들어달라고 요청하고, 그걸 통해서 단방향 통신을 구성하는 방법이다.

mkfifo 명령을 사용하면 파이프를 만들 수 있다.

mkfifo pipe1

사실은 그냥 파일이다.


이건 읽으려고 시도하면 아무것도 나오지 않는 채로 블락된다. 좀 특수한 파일이라서 읽으려는 행위를 하면 뭔가 들어올 때까지 대기하게끔 되어있다.

이 상태에서 저기에 데이터를 밀어넣으면

echo "Hello, World!" > pipe1

그때서야 데이터가 들어오고 읽기 작업이 종료된다.

이런 원리를 이용해서 프로세스 간에 데이터를 전달하는 것이다.
아래는 pipe1에 데이터를 보내는 간단한 코드다.

use std::error::Error;

use tokio::{io::AsyncWriteExt, net::unix::pipe};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let mut sender = pipe::OpenOptions::new().open_sender("./pipe1")?;

    sender.write_all("hello".as_bytes()).await?;

    Ok(())
}

그러면 받아질 것이다.

받는 것도 어렵지 않다. 파이프에 대해서 read를 시도하면

use std::error::Error;

use tokio::{io::AsyncReadExt, net::unix::pipe};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let mut receiver = pipe::OpenOptions::new().open_receiver("./pipe1")?;

    let mut buf = vec![0; 1024];
    let _ = receiver.read_buf(&mut buf).await?;

    let text = String::from_utf8_lossy(&buf);
    println!("Received: {}", text);

    Ok(())
}

데이터가 들어올 때까지 블럭되다가


들어오면 그때서야 읽고 종료된다.




소켓 통신을 통한 공유 (All OS)

또 하나의 방법은 TCP나 UDP 같은 소켓을 열어서 로컬 내부통신을 통해 동기화를 수행하는 것이다.

어차피 내부통신이면 네트워크를 타지 않고 파일시스템만 거치니까, 위의 pipe 같은 방식과 성능차이도 크게 나지는 않을 것이다.

게다가 OS 호환성도 좋다. 대부분의 OS에서 다 잘 작동할 것이다.




공유 메모리 (Shared Memory)

대부분의 OS에서 다 제공되기도 하고, 성능적으로도 가장 빨라서 IPC에서는 지배적으로 사용되는 동기화 매커니즘이다.
이게 뭐냐면, OS에 요청해서 실제로 여러 프로세스들이 공유할 수 있는 메모리를 할당해놓고 그걸 사용하는 것이다.

https://www.linkedin.com/pulse/interprocess-communicationipc-using-shared-memory-pratik-parvati
disk를 사용하지 않고 메모리에 바로 액세스하기 때문에 성능적으로 빠르다.

대신 같은 공간에 쓰거나 하는 충돌에 대해서는 직접 제어를 해야 한다.

대표적으로 Python에서 사용하는 멀티프로세싱에서는 shared memory를 통해서 동기화를 수행한다.
이런 느낌이다.

https://blog.naver.com/sssang97/223418963078



참조
https://stackoverflow.com/questions/6388031/multithreading-vs-multiprocessing
https://stackoverflow.com/questions/7186876/whats-the-difference-between-named-pipe-and-mailslot-mailbox
https://en.wikipedia.org/wiki/Named_pipe
https://www.linkedin.com/pulse/interprocess-communicationipc-using-shared-memory-pratik-parvati
https://3tilley.github.io/posts/simple-ipc-ping-pong/