본문 바로가기
Computer Science/운영체제(Operating System)

멀티프로세스와 멀티스레드

by 컴돈AI 2024. 4. 2.

목차

    멀티프로세스(multiprocess)와 멀티스레드(multithread)

    • 멀티프로세스와 멀티스레드 방식으로 프로그래밍을 진행하게 되면 코어가 여러 개일 경우 병렬성을 가집니다. (컴퓨터 환경이 단일 코어일 경우에는 동시성만 가집니다.) 
    • 멀티프로세스 : 여러 프로세스를 동시에 실행하는 것
    • 멀티스레드 : 여러 스레드로 하나의 프로세스를 동시에 실행하는 것
    • 프로세스끼리는 기본적으로 자원을 공유하지 않지만, 스레드끼리는 같은 프로세스 내의 자원을 공유합니다.

    멀티프로세스

    • 멀티프로세스
      • 프로세스를 fork하여 같은 작업을 하는 동일한 프로세스 두 개를 동시에 실행하면 코드 영역, 데이터 영역, 힙 영역 등을 비롯한 모든 자원이 복제되어 메모리에 적재됩니다.
        • 한 마디로 PID, 저장된 메모리 주소를 제외하면 모든 것이 동일한 프로세스 두 개가 통째로 메모리에 적재되는 것입니다.
        • 프로세스가 여러개 생기면 그만큼 메모리에 복제가 됩니다. → 이는 자원 낭비입니다.
        • 따라서 fork를 한 직후 같은 프로세스를 통째로 메모리에 중복 저장하지 않으면서 동시에 프로세스끼리 자원을 공유하지 않는 방법도 있습니다.  → 쓰기 시 복사(COW, Copy On Write)
      • 기본적으로 프로세스는 독립적으로 실행되기 때문에 자원을 공유하지 않지만, 프로세스간 통신(IPC: Inter-Process Communication)을 사용하여 프로세스 간의 자원을 공유하고 데이터를 주고받을 수 있습니다.
        • 하나의 파일을 통해 프로세스간 통신 가능
          • 예를 들어 note.txt 에 한 프로세스는 값을 쓰고, 한 프로세스는 값을 읽을 수 있습니다.
        • 공유 메모리
          • 프로세스들은 서로 공유하는 메모리 영역(= 공유 메모리)을 두어 데이터를 주고받을 수 있습니다.
          • 예를 들어 프로세스 A와 B가 공유하는 메모리 영역 내에 name이라는 전역 변수가 있다고 가정할 때, 프로세스 A가 name안에 값을 저장한 뒤, 프로세스 B가 name 변수 값을 읽어 들일 수 있습니다.
        • 소켓, 파이프 등을 통한 통신
        • 프로세스마다 각자 메모리 영역을 가지는 것을 확인할 수 있습니다.

    멀티스레드

    • 멀티스레드
      • 스레드들은 각기 다른 스레드 ID, 프로그램 카운터 값을 포함한 레지스터값, 스택을 가질뿐 프로세스가 가지고 있는 자원을 공유합니다.
        • 즉, 동일한 주소 공간의 코드, 데이터, 힙 영역을 공유하고, 열린 파일과 같은 프로세스 자원을 공유합니다.
        • 이로 인해 여러 프로세스를 병행 실행하는 것보다 메모리를 더 효율적으로 사용할 수 있습니다.
        • 프로세스의 자원을 공유하기 때문에 서로 협력과 통신에 유리합니다.
        • 하나의 스레드에 문제가 생기면 프로세스 전체에 문제가 생길수 있는 단점이 있습니다.
        • 스레드의 경우, 각자 코드부분이 어디인지 알아야 하기 때문에 스레드별로 프로그램 카운터 값을 비롯한 레지스터 값들과 각자 자신만의 스택영역(ex. 지역변수)을 가집니다.

    파이썬에서의 멀티스레드 (With GIL)

    • 파이썬의 경우 멀티스레드로 코드를 작성하더라도 CPU Bound 작업에서는 멀티 코어의 효과를 기대할 수 없습니다. 즉, 하나의 코어로 여러 스레드를 번갈아가며 실행하게 됩니다.
    • Python은 GIL(Global Interpreter Lock)이라는 특징이 있습니다.
      • Python의 경우 참조 카운팅(Reference Counting, RC)을 메모리 관리 기법으로 사용합니다. 따라서 여러 스레드가 동시에 객체 참조 카운트를 변경할 경우, 동기화 문제(ex. 경쟁 상태, 데드락)가 발생할 수 있습니다. 
      • 파이썬은 이러한 문제를 방지하기 위해 GIL을 도입했습니다.
      • GIL은 멀티스레드 프로그램이 실행될 때, 한 번에 하나의 스레드만이 Python 객체에 접근할 수 있도록 제한(Lock)합니다. 이를 멀티 스레드 환경에서 발생할 수 있는 동기화 문제를 예방할 수 있습니다.
        • 즉, 멀티스레드 환경이라도 한번에 하나의 코어가 번갈아가며 실행됩니다. (한 번에 여러 개의 스레드가 한 객체에 접근하는 것을 제한하기 위함.)
    • Python의 멀티스레딩은 GIL로 인해 한 번에 하나의 코어가 여러개의 스레드를 번갈아가며 실행하게 됩니다. (즉, Python의 경우, 다른 언어와 달리 코어가 여러개더라도 멀티 스레드에서 단일 코어만을 이용하여 진행하게 됩니다.)
      • 예시
        • 멀티스레드를 이용하여 4개의 CPU Bound 작업을 처리할 때 Python과 다른 언어의 차이 
          • Python : 4개의 CPU Bound 작업을 하나의 코어가 번갈아가며 조금씩 실행하게 됩니다.(동시성)
          • 다른 언어 : 4개의 CPU Bound 작업을 여러 개의 코어가 동시에 처리하게 됩니다.(단, 코어가 4개 이상일 경우) (병렬성) 
          • 결국 시간은 Python이 다른 언어에 비해 약 4배 정도 걸리게 될 것입니다.
        • 멀티스레드를 이용하여 4개의 I/O Bound 작업을 처리할 때 Python과 다른 언어의 차이
          • Python : 4개의 I/O Bound 작업을 하나의 코어가 번갈아가며 실행하게 됩니다. 단, 이 경우에는 해당 작업이 I/O Bound 작업일 경우 GIL이 해제되어 이 시간 동안 다른 스레드가 CPU를 사용할 수 있게 합니다. 즉, 각 스레드는 I/O 작업을 시작하고, 해당 작업이 완료될 때까지 대기합니다. 대기하는 동안, GIL은 다른 스레드가 실행을 계속할 수 있도록 해제됩니다. 
          • 따라서 I/O Bound 작업의 경우 다른 언어와 시간적 차이가 거의 존재하지 않습니다.
    • 참고로 I/O Bound 작업 외에도 Python이 하나의 프로그램에서 GIL의 영향을 받지 않는 경우가 있습니다. 
      • Python 라이브러리 중 일부 c나 c++로 작성된 경우에는 GIL을 잠시 해제하고 CPU 바운드 작업을 멀티 코어 환경에서 병렬로 처리할 수 있게 해 줍니다. (ex. Numpy) 

    파이썬에서의 멀티스레드와 비동기 프로그래밍의 차이

    • 파이썬에서의 멀티스레드 방식이 단일 코어로 진행된다면 비동기 프로그래밍과 차이가 없지 않나?라는 생각이 들 수 있습니다. 하지만 비동기 프로그래밍은 단일 스레드로 진행되기 때문에 명백한 차이가 존재합니다. 각자의 상황이 유리한 경우의 예시를 살펴보면서 차이를 살펴보도록 하겠습니다.
    • 멀티스레드가 유리한 경우
      • 멀티스레드의 경우
        • 5개의 스레드를 생성하고, A,B,C,D,E 작업을 실행합니다. 먼저 CPU를 A가 할당받고 A는 I/O 작업이기 때문에 GIL을 해제하여 곧바로 다른 스레드가 CPU를 사용할수 있게 합니다. B,C,D,E 모두 I/O 작업이기때문에 모든 작업에 대해 I/O작업이 끝날 때까지 CPU는 사용되지 않습니다. 그러다가 E의 I/O작업이 가장 먼저 종료되게 되고 CPU는 온전히 E만을 위해서 일을 합니다. 그러다가 B 역시 I/O 작업이 끝나게 되고 B와 E는 서로 CPU를 번갈아가며 일을 수행하게 될 것입니다. 이런 식으로 다른 I/O 작업들이 끝나면 모든 작업들이 서로 CPU를 번갈아가며 각 작업들이 동시에 진행되는 것처럼 보일 것입니다.
        • 만약 파이썬이 아니라 다른 언어였다면 5개의 코어를 곧바로 사용하여 A,B,C,D,E가 각자의 스레드별로 쭉 실행될것입니다. (파이썬의 경우 멀티스레드방식이여도 1개의 코어만 사용, 다른 언어는 여러개의 코어 사용 가능)
      • 비동기 프로그래밍의 경우
        • 비동기 프로그래밍의 경우 A,B,C,D,E 작업의 I/O 작업에 대해서 await를 통해 해당 I/O 작업이 처리되는 동안 다른 작업을 처리할 수 있도록 설정해 줍니다. 그러다가 이제 E의 I/O작업이 완료되면 비동기 프로그래밍의 경우 단일 스레드이기 때문에 해당 CPU 작업이 완료될 때까지는 A,B,C,D에 대해서 어떠한 작업도 처리하지 못합니다. 그러다가 CPU 작업이 완료되면 다음으로 I/O 작업이 끝난 B에 대해서 CPU 작업을 처리하고, 순차적으로 A,D,E 순서대로 CPU작업을 처리합니다. 결국 한 작업이 완료되어야지만 다른 작업이 수행될 수 있습니다. 
          • CPU Bound 작업을 만날 경우, 해당 작업을 처리할 때까지는 다른 I/O 작업이 끝나더라도 처리할 수 없습니다.
        • 이처럼 비동기 프로그래밍은 중간에 CPU Bound 작업을 만나게 된다면 원하는 대로 데이터 처리가 이루어지지 않을 수가 있습니다.
    • 비동기 프로그래밍이 유리한 경우
      • 멀티스레드의 경우
        • 1000개의 작업이 있기 때문에 모든 I/O작업을 병렬적으로 처리하기 위해서는 스레드를 1000개 생성해야 합니다. 하지만 이렇게 스레드를 많이 생성하는 것은 상당한 양의 메모리가 소비되고 많은 비용이 발생합니다. 
        • 보통 이럴 경우 스레드풀 방식으로 미리 스레드의 개수를 몇 개(일반적으로 코어개수만큼) 생성해 처리합니다.  스레드풀 방식을 통해 작업 큐에서 작업을 가져와서 스레드에 할당하고, 작업이 완료되면 다른 대기 중인 작업으로 교체됩니다
          • 스레드 풀(Thread Pool)
            • 미리 생성된 스레드의 집합
            • 여러 작업을 처리하기 위해 사용
            • 실제 작업에 사용될 스레드를 미리 여러 개 생성해 두고 재사용함으로써 작업이 발생할 때마다 새로운 스레드를 만드는 오버헤드를 줄이는 것입니다.
        • 스레드 풀 방식을 이용하더라도 만약 코어가 10개라 10개의 스레드 풀을 이용해 처리한다고 가정하면, 이 경우 총 소요시간은 (1000/10=100) 개 작업시간만큼 걸리게 될 것입니다.(모든 작업 시간이 동일하다고 가정)
          • 아주 많은 I/O 작업에서는 비효율적
      • 비동기 프로그래밍의 경우
        • 1000개의 작업이 모두 I/O 작업이기 때문에 그냥 비동기적으로 각 함수마다 await를 통해 I/O 작업을 처리하게 할 수 있습니다. 
        • 그냥 I/O작업만 실행해 놓으면 1000개의 작업이 I/O작업을 다 처리할 때까지 대기하고 있을 수 있습니다.(즉, I/O 작업용 스위치를 가지고 있다고 생각하면 됩니다.)
    • 정리
      • 파이썬 멀티스레드는 각자의 레일이 여러 개 있고, 그 레일 위의 작업을 하는 로봇 한대가 있는 것입니다.(파이썬은 멀티스레드더라도 단일코어로 진행됨.)
        • 각 레일에 I/O작업이 올라올 경우 해당 I/O 작업이 처리될 때까지 로봇은 다른 레일 위의 작업들을 대신 더 많이 수행할 수 있습니다.
        • 참고 : 다른 언어의 멀티스레드의 경우 각자의 레일이 여러 개 있고, 각 레일별로 작업하는 로봇이 한 대씩 있는 것입니다.
      • 비동기 프로그래밍은 하나의 레일만 가지고 있습니다. 마찬가지로 해당 레일 위의 작업을 하는 로봇 한대가 있습니다. (단일 스레드, 단일 코어)
        • 이 레일은 대신 특수한 기능을 가지고 있습니다. I/O작업이 레일 위로 올라왔을 때 실행버튼을 눌러두고, 잠시 레일 위에서 해당 I/O 작업을 내려둘 수 있습니다. 비어 있는 레일 위로 다른 작업들을 계속 이어서 받을 수 있습니다. 중간중간 I/O 작업이 끝나면 그 뒤의 작업들도 이어서 레일 위로 올리게 됩니다. 이런 식으로 하나의 처리 흐름을 따라 진행되게 됩니다. 레일이 하나이기 때문에 앞서 레일 위로 올라간 CPU 작업들이 먼저 처리되어야지만 다음 작업들이 처리가 될 수 있습니다. (만약 중간에 CPU를 오래 사용하는 작업이 걸리게 되면, 해당 레일은 그 작업을 처리하느라 다른 어떤 작업들도 처리하지 못합니다.)
      • 여기서 주의할 점은 멀티스레드랑 비동기 프로그래밍은 따로 따로 사용하는 것이 아니라, 둘을 적절하게 섞어서 프로그래밍할 수 있다는 것을 알아야합니다.

     

    멀티프로세스 vs 멀티스레드

    • 둘의 가장 큰 차이점은 자원 공유 여부입니다. 아래의 장단점은 모두 자원 공유 여부로부터 발생한 것입니다.
      • 멀티프로세스
        • 장점 : 안전성, 확장성 
        • 단점 : 프로세스 간 소통이 비교적 무겁고, 프로세스 생성 비용이 큼
      • 멀티스레드
        • 장점 : 자원을 공유하므로 스레드 간 데이터 공유 용이, 스레드 생성 비용이 프로세스에 비해 상대적으로 낮음, 자원을 공유하므로 프로세스에 비해 메모리 절약
        • 단점 : 자원을 공유하므로 동기화 문제 주의, 하나의 스레드가 전체 프로세스에 영향을 줄 수 있는 위험 존재 

    출처