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

Bound | Blocking(Non-Blocking) | Multiprocessing vs Multithreading vs Asynchronous Programming

by 컴돈AI 2023. 12. 15.

목차

    Bound

    • "Bound"는 프로그램이나 시스템의 성능이 특정 자원에 의해 제한되는 상태를 의미합니다.
    • 대표적으로 CPU Bound와 IO Bound가 존재합니다.

    CPU Bound

    • CPU Bound는 프로그램이나 시스템의 성능이 CPU의 처리(계산) 능력에 의해 제한되는 경우를 말합니다.
    • CPU Bound 상황에서는 CPU가 연산 작업에 바쁘게 동작하며, 프로그램의 속도는 CPU의 처리 속도에 의해 결정됩니다.
    • CPU Bound 예시
      • 수학적 계산 : 대규모 수학적 연산, 복잡한 수학 문제 해결 등
      • 데이터 압축 : 파일이나 데이터의 압축 및 압축 해제 과정은 CPU를 집중적으로 사용합니다.
    • CPU Bound 상황에서는 더 강력한 CPU를 사용하거나, 병렬처리를 통해 성능을 향상할 수 있습니다.

    IO Bound

    • IO Bound는 프로그램의 성능이 입출력 작업(디스크 읽기/쓰기, 네트워크 통신 등)에 의해 제한될 때 발생합니다. 
    • IO Bound 상황에서는 CPU가 데이터를 읽거나 쓰는 작업을 기다리는 동안 대부분의 시간을 "대기" 상태로 보내게 되며, 이로 인해 CPU의 계산 능력이 완전히 활용되지 않습니다.
    • IO Bound 예시
      • 파일 시스템 작업 : 디스크에서 큰 파일을 읽거나 쓰는 작업
      • 데이터베이스 쿼리 : 대용량 데이터베이스에서 쿼리를 실행하는 것은 IO Bound 작업
      • 웹페이지 로딩 : 서버로부터 웹 페이지를 로드하는 것은 네트워크 IO에 의존하므로 IO Bound에 속합니다.
      • 스트리밍 데이터 처리 : 실시간으로 데이터를 스트리밍 하는 경우, 네트워크 속도에 의해 성능이 제한될 수 있습니다.
    • IO Bound 상황 개선 방법
      • 비동기 프로그래밍 : 비동기 프로그래밍을 통해 IO 작업이 완료되기를 기다리는 동안 CPU가 다른 작업을 계속 진행할 수 있도록 해줍니다.
      • 캐싱 : 자주 사용되는 데이터를 캐시에 저장하여 빈번한 디스크 접근을 줄일 수 있습니다.
      • 데이터 압축 및 최적화 : 전송되는 데이터의 크기를 최소화하여 IO 작업 시간을 줄일 수 있습니다.

    Blocking - NonBlocking

    Blocking

    • "Blocking"은 프로그램의 특정 부분이나 함수가 다른 작업이 완료될 때까지 대기하는 상태를 나타내는 용어입니다.
    • 이 용어는 전체 프로그램에 국한되지 않으며, 특정 작업이나 함수에 대해 사용될 수 있습니다. 
      • 참고
        • Bound 상황은 성능의 제한 요소를 나타내고, Blocking은 실행 흐름이 중단되는 상황을 의미합니다.
        • 즉, ~ Bound는 ~때문에 프로그램의 성능이 제한되는 상황을 의미하고, Blocking은 어떤 작업을 실행했을 때 그 작업으로 인하여 현재 함수나 프로그램의 특정 부분의 실행 흐름이 중단(대기)하는 상태를 의미합니다.
        • 정확한 비교
          • CPU Bound 작업에서의 Blocking
            • 함수 A (CPU Bound 작업 수행): CPU Bound 상황에서 CPU는 계산 작업에 바쁘게 동작하고 있으므로, 함수 A 자체는 Blocking 상태가 아닙니다.  
            • 함수 B에서 함수 A 호출: 함수 B가 함수 A를 호출하고, 함수 A의 작업 완료를 기다리는 동안 함수 B는 다른 작업을 수행할 수 없습니다. 이 경우, 함수 A는 함수 B에 대해 Blocking 작업입니다.   
          • IO Bound 작업에서의 Blocking
            • 함수 A (IO Bound 작업 수행): 웹 서버로부터 응답을 받는 IO Bound 작업을 수행하는 경우, 해당 작업이 완료될 때까지 함수 A는 다른 작업을 수행할 수 없습니다. 따라서, 이 IO 작업은 함수 A에 대해 Blocking 작업입니다.
            • 함수 B에서 함수 A 호출: 함수 B가 함수 A를 호출하고, 함수 A의 작업 완료를 기다리는 동안 함수 B는 다른 작업을 수행할 수 없습니다. 이 경우도 마찬가지로 함수 A는 함수 B에 대해 Blocking 작업입니다.
    • Blocking 예시
      • 함수 호출 : 예를 들어 어떤 함수 'B'가 실행되는 도중에 CPU Bound 작업을 수행하는 함수인 'A'를 호출하면, 'A'가 완료될 때까지 'B'의 실행이 중단되게 됩니다. 이러한 경우 'A'는 'B'에 대해 Blocking 작업입니다.
      • IO 작업 : 파일을 읽거나 쓰는 동안 프로그램이 다른 작업을 수행할 수 없는 경우, 이 파일 IO 작업은 Blocking 작업입니다.
      • 네트워크 요청 : 웹 서버로부터 응답을 받을 때까지 프로그램이 기다리는 경우, 이 네트워크 요청은 Blocking 작업입니다.

    NonBlocking

    • "Non-Blocking"은 특정 함수나 작업이 다른 작업의 완료를 기다리지 않고 다른 작업을 계속 수행할 수 있는 상태를 의미합니다.
    • 비동기 프로그래밍을 통해 Non-Blocking을 구현할 수 있습니다.
      • 아래 예시들에서 IO 작업으로 인해 대기하는 상태를 asyncio.sleep()을 사용하여 표현하였습니다.
      • 코드
        • 하나의 함수가 완료되지 않더라도 다음 함수가 실행되는 것을 확인할 수 있습니다. 즉, IO Bound 작업으로 인해 Blocking 되지 않는 상태입니다.(Non-Blocking)
          import asyncio
          
          async def io_bound_task(num):
              # IO Bound 작업을 수행합니다.
              print(f"IO Bound {num} 작업 시작")
              await asyncio.sleep(2)  # IO 작업 시뮬레이션
              print(f"IO Bound {num} 작업 완료")
              return "IO 결과"
          
          async def main():
              task1 = asyncio.create_task(io_bound_task(1))
              task2 = asyncio.create_task(io_bound_task(2))
              task3 = asyncio.create_task(io_bound_task(3))
          
              await asyncio.gather(task1, task2, task3)
          
          asyncio.run(main())
          
          -->
          IO Bound 1 작업 시작
          IO Bound 2 작업 시작
          IO Bound 3 작업 시작
          IO Bound 1 작업 완료
          IO Bound 2 작업 완료
          IO Bound 3 작업 완료
    • 하지만 비동기라고 해서 무조건 Non-Blocking인것은 아닙니다.
      • 비동기이면서 Blocking인 코드를 살펴보면 다음과 같습니다.
        • CPU Bound 작업을 수행하는 함수는 비동기적으로 실행되더라도 효과를 볼 수 없습니다. 이럴 경우는 멀티프로세싱이나 멀티스레딩을 이용해야 효율적입니다. (파이썬의 경우 GIL로 인해 멀티프로세싱에서만 효율적입니다.)
        • 아래 코드결과를 살펴보면 CPU Bound 작업을 수행하는 함수(task1)가 끝나야지만 task1, task2가 비동기적으로 실행되는 것을 확인할 수 있습니다.
        • 이럴 경우 CPU Bound 작업을 수행하는 함수(task1)에 의해 main 함수가 Blocking 되는 경우입니다.
        • import asyncio
          
          async def cpu_bound_task():
              # CPU Bound 작업을 수행합니다.
              print("CPU Bound 작업 시작")
              result = sum(i * i for i in range(10000000)) # 복잡한 계산을 시뮬레이션
              print("CPU Bound 작업 완료")
              
              return result
          
          async def io_bound_task(num):
              # IO Bound 작업을 수행합니다.
              print(f"IO Bound {num} 작업 시작")
              await asyncio.sleep(2)  # IO 작업 시뮬레이션
              print(f"IO Bound {num} 작업 완료")
              return "IO 결과"
          
          async def main():
              task1 = asyncio.create_task(cpu_bound_task())
              task2 = asyncio.create_task(io_bound_task(1))
              task3 = asyncio.create_task(io_bound_task(2))
          
              await asyncio.gather(task1, task2, task3)
          
          asyncio.run(main())
          
          -->
          CPU Bound 작업 시작
          CPU Bound 작업 완료
          IO Bound 1 작업 시작
          IO Bound 2 작업 시작
          IO Bound 1 작업 완료
          IO Bound 2 작업 완료
    • 정리하면 IO Bound 작업은 비동기 프로그래밍으로 큰 효과를 볼 수 있지만(Async-NonBlocking), CPU Bound 작업은 비동기 프로그래밍만으로 큰 효과를 보기 어렵습니다(Async-Blocking). 따라서 CPU Bound 작업은 비동기 프로그래밍보다는 멀티프로세싱이나 멀티스레딩을 이용해야 합니다. (파이썬의 경우 GIL로 인해 멀티프로세싱에서만 효율적입니다.)

    멀티프로세싱(Multiprocessing)  vs 멀티스레딩(Multithreading) vs 비동기 프로그래밍(Asynchronous Programming)

    • 멀티프로세싱(Multiprocessing)
      • 여러 개의 프로세스를 사용하여 작업을 동시에 수행
      • 각 프로세스는 독립된 메모리 공간을 가집니다.
        • 각 프로세스가 독립된 메모리 공간을 가지기 때문에, 메모리 공간을 공유하는 멀티스레딩과 같은 방식으로 동기화를 고려할 필요가 적습니다.(프로세스 간 통신이 있을 경우에는 동기화를 고려해야 합니다.)
        • 하지만 각 프로세스가 독립된 메모리 공간을 가지기 때문에 프로세스 간 데이터를 공유하거나 통신해야 할 경우, 프로세스 간 통신(IPC) 메커니즘을 사용해야 합니다. 파이썬에서는 multiporcessing 모듈 내의 Queue, Pipe, Value, Array 등을 이용해 이러한 동기화 및 데이터 공유를 처리할 수 있습니다.
      • 프로세스 생성과 관리, 프로세스 간 통신에 상대적으로 높은 오버헤드가 발생합니다.
      • CPU Bound 작업에 효과적입니다. 각 프로세스가 별도의 CPU 코어를 사용하여 병렬 처리를 할 수 있습니다.
      • IO Bound 작업의 경우는 각 프로세스가 자신만의 메모리 공간을 가지고 별도로 관리되어야 하고 프로세스 관리로 인해 오버헤드가 발생하여 멀티스레딩이나 비동기 프로그래밍에 비해 상대적으로 이점이 적을 수 있습니다.
    • 멀티스레딩(Multithreading)
      • 단일 프로세스 내에서 여러 스레드를 사용하여 작업을 동시에 수행
      • 모든 스레드는 같은 메모리 공간을 공유합니다. 따라서 공유된 메모리에 대한 동기화가 필요합니다.
      • 스레드 생성과 관리에 오버헤드가 발생합니다. 하지만 멀티프로세싱에 비해 비교적 낮은 오버헤드가 발생합니다.
      • CPU Bound 작업에 효과적(단, 파이썬의 경우 GIL(Global Interpreter Lock)으로 인해 한 시점에 하나의 스레드만이 실행되어서 파이썬의 멀티스레딩은 CPU Bound 작업에 대해 병렬 처리의 이점이 없습니다.)
      • IO Bound 작업에 효과적 (IO Bound 작업 대기 시간 동안 다른 작업을 수행할 수 있음)
        • 단, 비동기 프로그래밍과 비교하면 스레드 생성과 관리에 대한 오버헤드가 발생하기 때문에 단순히 IO Bound 작업에 대해서는 비동기 프로그래밍보다는 비효율적입니다.
        • 하지만 CPU Bound와 IO Bound가 혼합된 작업에서는 CPU Bound 작업에 대해 Blocking이 되는 비동기 프로그래밍과 달리 멀티스레딩은 Non-Blocking으로 작업이 되어 보다 효율적입니다.
    • 비동기 프로그래밍(Asynchronous Programming)
      • 단일 프로세스 내에서 단일 스레드를 사용하여 작업을 수행
      • 단일 스레드내에서 작업이 이루어지므로, 동기화 문제가 없습니다.
      • CPU Bound 작업에서는 아무런 이점이 없음(비동기적으로 코드를 짜도 Blocking이 걸림)
      • IO Bound 작업에서 효과적(IO Bound 작업 대기 시간 동안 다른 작업을 수행할 수 있음)
    • 정리
      • CPU Bound 작업 : 멀티스레딩 or 멀티프로세싱
        • 일반적으로 프로세스 오버헤드로 인해 멀티스레딩이 효율적입니다.
        • 파이썬의 경우 GIL로 인하여 멀티프로세싱이 효율적입니다.
          • 멀티 스레딩의 경우 각각의 스레드가 자신의 작업을 번갈아가며(동시성) 실행하게 됩니다.
        • 작업이 완전히 독립적이거나 복잡한 동기화가 필요한 경우 멀티프로세싱이 유리할 수 있습니다.
      •  IO Bound 작업 : 비동기 프로그래밍 or 멀티스레딩
        • 기본적으로 스레드 오버헤드가 발생하는 멀티스레딩보다 비동기 프로그래밍이 효율적입니다. 
          • 또한 멀티 스레딩의 경우 각각의 스레드가 자신의 작업을 번갈아가며(동시성) 실행하는 것이기 때문에, 실제로는 각각의 스레드는 자신의 IO 작업에서 대기상태로 있게 됩니다.(각 스레드가 독립적으로 작동) 즉, 비동기 프로그래밍처럼 IO 작업이 대기상태면 다른 작업을 수행하는 것이 아닌 대기상태 그대로 있게 됩니다. 단지 스레드가 여러 개라서 그 각각의 스레드가 번갈아 가며 실행되다 보니 비동기 프로그래밍처럼 IO 작업을 대기하지 않고 다른 작업을 수행하는 것처럼 보이게 되는 것입니다. 
          • 예를 들어 총 50개의 시간이 오래걸리는 I/O 작업이 있다고 가정해 보겠습니다. 비동기 프로그래밍의 경우 이 50개의 I/O 작업을 모두 실행시켜 놓을 수 있습니다.(비동기 프로그래밍은 await를 사용하여 I/O 작업이 진행되는 동안 프로그램의 실행 흐름을 차단하지 않고 다른 작업으로 전환할 수 있습니다.) 하지만 멀티 스레딩의 경우(10개의 스레드라고 가정) 10개의 스레드가 모두 I/O 작업을 진행하게 되면 모두 대기상태로 그 I/O 작업이 완료될 때까지 있게 됩니다.(다른 40개 작업으로 전환하지 못합니다.) 그러므로 I/O Bound 위주의 작업에서는 비동기 프로그래밍에 비해 멀티스레딩이 비효율적이게 됩니다.
        • 하지만 만약 사용하려는 라이브러리나 함수가 비동기적으로 작성되지 않았다면, 이러한 함수들은 기본적으로 동기적으로 작동합니다. 이럴 경우는 스레드 생성과 관리에 오버헤드가 발생하더라도 멀티스레딩으로 코드를 작성해야 합니다.
        • 예를 들어 HTTP 요청을 할 때 requests 라이브러리를 사용하게 되면 requests 라이브러리는 동기적으로 작성되어 있어 비동기적으로 코드를 작성하더라도 동기적으로 작동하게 됩니다. 따라서 HTTP 요청을 비동기적으로 처리하려면 비동기적으로 작성된 라이브러리인 aiohttp를 사용해야 합니다.
      • CPU Bound + IO Bound 혼합 작업 : 멀티스레딩 or 멀티프로세싱
        • 일반적으로 프로세스 오버헤드로 인해 멀티스레딩이 효율적입니다.
        • 파이썬의 경우 GIL로 인하여 CPU Bound 위주일 경우 멀티프로세싱이 효율적이고,  IO Bound 위주일 경우는 멀티스레딩이 효율적입니다.
        • CPU Bound 위주이면서, 작업이 완전히 독립적이거나 복잡한 동기화가 필요한 경우 멀티프로세싱이 유리할 수 있습니다.
        • 참고 : CPU Bound + IO Bound 혼합 작업에서의 멀티스레딩 코드
          • 앞선 예시에서 CPU Bound + IO Bound 혼합 작업을 비동기 프로그래밍으로 작업하면 CPU Bound 함수로 인해 Blocking이 일어나는 것을 관찰했습니다.
          • 하지만 멀티스레딩을 이용하여 코드를 작성하게 되면 CPU Bound + IO Bound 혼합 작업에서 Blocking이 일어나지 않게 됩니다.
          • from concurrent.futures import ThreadPoolExecutor
            import time
            
            def cpu_bound_task():
                print("CPU Bound 작업 시작")
                result = sum(i * i for i in range(10000000))
                print("CPU Bound 작업 완료")
                return result
            
            def io_bound_task(num):
                print(f"IO Bound {num} 작업 시작")
                time.sleep(2)  # IO 작업 시뮬레이션
                print(f"IO Bound {num} 작업 완료")
                return f"IO 결과 {num}"
            
            # ThreadPoolExecutor 인스턴스 생성
            executor = ThreadPoolExecutor()
            
            # map을 사용하여 여러 작업을 동시에 실행하고 결과를 수집
            results = list(executor.map(lambda x: io_bound_task(x) if x != 0 else cpu_bound_task(), range(3)))
            
            # 스레드 풀 종료
            executor.shutdown()
            
            # 결과 출력
            for result in results:
                print(result)
                
            -->
            CPU Bound 작업 시작
            IO Bound 1 작업 시작
            IO Bound 2 작업 시작
            CPU Bound 작업 완료
            IO Bound 1 작업 완료
            IO Bound 2 작업 완료
            333333283333335000000
            IO 결과 1
            IO 결과 2

    IO Bound 작업에서 Asynchronous Programming vs Multi threading 

    • 기본 (약 20초 소요)
      • import requests
        import time
        import os
        import threading
        
        
        def fetcher(session, url):
            print(f"{os.getpid()} process | {threading.get_ident()} url : {url}")
            with session.get(url) as response:
                return response.text
        
        
        def main():
            urls = ["https://google.com", "https://naver.com"] * 50
        
            with requests.Session() as session:
                result = [fetcher(session, url) for url in urls]
        
        if __name__ == "__main__":
            start = time.time()
            main()
            end = time.time()
            print(end - start)
            
        -->
        41664 process | 5492 url : https://google.com
        41664 process | 5492 url : https://naver.com
        41664 process | 5492 url : https://google.com
        ...
        41664 process | 5492 url : https://naver.com
        41664 process | 5492 url : https://google.com
        41664 process | 5492 url : https://naver.com
        20.55317521095276
    • Asynchronous Programming (약 2.2초 소요)
      • import aiohttp
        import time
        import asyncio
        import os
        import threading
        
        
        async def fetcher(session, url):
            print(f"{os.getpid()} process | {threading.get_ident()} url : {url}")
            async with session.get(url) as response:
                return await response.text()
        
        
        async def main():
            urls = ["https://google.com", "https://naver.com"] * 50
        
            async with aiohttp.ClientSession() as session:
                result = await asyncio.gather(*[fetcher(session, url) for url in urls])
        
        
        if __name__ == "__main__":
            start = time.time()
            asyncio.run(main())
            end = time.time()
            print(end - start)
            
        -->
        40716 process | 12292 url : https://google.com
        40716 process | 12292 url : https://naver.com
        40716 process | 12292 url : https://google.com
        ...
        40716 process | 12292 url : https://naver.com
        40716 process | 12292 url : https://google.com
        40716 process | 12292 url : https://naver.com
        2.2053892612457275
    • Multi threading  (약 6.5초)
      • import requests
        import time
        import os
        import threading
        from concurrent.futures import ThreadPoolExecutor
        
        
        def fetcher(params):
            session = params[0]
            url = params[1]
            print(f"{os.getpid()} process | {threading.get_ident()} url : {url}")
            with session.get(url) as response:
                return response.text
        
        
        def main():
            urls = ["https://google.com", "https://naver.com"] * 50
        
            executor = ThreadPoolExecutor(max_workers=10)
        
            with requests.Session() as session:
                params = [(session, url) for url in urls]
                results = list(executor.map(fetcher, params))
        
        
        if __name__ == "__main__":
            start = time.time()
            main()
            end = time.time()
            print(end - start)
            
        -->
        9356 process | 43824 url : https://google.com
        9356 process | 36648 url : https://naver.com
        9356 process | 16220 url : https://google.com
        9356 process | 29092 url : https://naver.com
        9356 process | 27556 url : https://google.com
        9356 process | 29092 url : https://naver.com
        9356 process | 36648 url : https://google.com
        ...
        9356 process | 20900 url : https://naver.com
        9356 process | 17612 url : https://google.com
        9356 process | 31852 url : https://naver.com
        6.4980669021606445
    • 정리
      • 시간 : 기본 >> 멀티 스레딩 >> 비동기 프로그래밍 
      • 멀티 스레드를 사용할 경우 스레드 오버헤드등으로 인하여 비동기 프로그래밍에 비해 시간이 더 소요되게 됩니다. 또한 멀티 스레딩의 경우는 하나의 스레드가 I/O 작업을 수행하고 있을 때, 해당 작업이 완료될 때까지 해당 스레드는 대기 상태에 있게 됩니다. 즉, 10개의 스레드를 사용하는데 10개의 스레드가 모두 I/O 작업 중 대기하는 상태이면 코루틴처럼 다른 I/O 작업을 수행할 수 있는 것이 아닌 10개의 스레드가 해당하는 I/O 작업이 끝나야지만 다른 I/O 작업을 수행할 수 있습니다. 따라서 온전히 I/O 작업일 경우에는 멀티 스레딩과 비동기 프로그래밍의 시간차이가 많이 발생하게 되는 것입니다.
        • 즉, 비동기 프로그래밍은 하나의 스레드이지만 하나의 스레드가 여러 개의 I/O 작업 스위치를 켜고 다닐 수 있습니다. (I/O 작업이 완료되지 않더라도 다음 I/O 작업 스위치를 킬 수 있습니다.)
        • 하지만 멀티 스레딩은 스레드가 여러 개이지만 각각의 스래드는 오로지 한 개의 I/O 작업 스위치를 켜고 나면 그 I/O작업이 완료될 때까지 다른 I/O 작업 스위치를 켜지는 못합니다. (원래는 멀티 프로세싱처럼 병렬로 처리되어야 하지만, 파이썬의 GIL로 인하여 병렬로 처리될 것이 동시성으로 하나씩 번갈아가며 실행되어 비동기처럼 보일뿐 실제로는 각 스레드가 동기적으로 작동하고 있는것입니다. 비동기 프로그래밍과는 다른 작동방식입니다.)

    CPU Bound 작업에서 Multi threading vs Multi processing

    • 기본 (약 12.3초 소요)
      • import time
        import os
        import threading
        
        nums = [30] * 100
        
        
        def cpu_bound_func(num):
            print(f"{os.getpid()} process | {threading.get_ident()} thread")
            numbers = range(1, num)
            total = 1
            for i in numbers:
                for j in numbers:
                    for k in numbers:
                        total *= i * j * k
            return total
        
        
        def main():
            results = [cpu_bound_func(num) for num in nums]
        
        
        if __name__ == "__main__":
            start = time.time()
            main()
            end = time.time()
            print(end - start)
            
        -->
        36756 process | 37392 thread
        36756 process | 37392 thread
        36756 process | 37392 thread
        ...
        36756 process | 37392 thread
        36756 process | 37392 thread
        36756 process | 37392 thread
        12.34961748123169
    • Multi threading (약 10.6초)
      • import time
        import os
        import threading
        from concurrent.futures import ThreadPoolExecutor
        
        nums = [30] * 100
        
        def cpu_bound_func(num):
            print(f"{os.getpid()} process | {threading.get_ident()} thread, {num}")
            numbers = range(1, num)
            total = 1
            for i in numbers:
                for j in numbers:
                    for k in numbers:
                        total *= i * j * k
            return total
        
        
        def main():
            executor = ThreadPoolExecutor(max_workers=10)
            results = list(executor.map(lambda num :cpu_bound_func(num), nums))
        
        
        if __name__ == "__main__":
            start = time.time()
            main()
            end = time.time()
            print(end - start)
            
        -->
        17728 process | 44652 thread, 30
        17728 process | 1984 thread, 30
        17728 process | 20896 thread, 30
        17728 process | 36892 thread, 30
        17728 process | 6868 thread, 30
        17728 process | 8168 thread, 30
        17728 process | 43012 thread, 30
        17728 process | 44652 thread, 30
        17728 process | 45784 thread, 30
        17728 process | 40232 thread, 30
        17728 process | 20896 thread, 30
        ...
        17728 process | 8168 thread, 30
        17728 process | 38000 thread, 30
        17728 process | 36892 thread, 30
        17728 process | 43012 thread, 30
        17728 process | 44652 thread, 30
        17728 process | 6868 thread, 30
        17728 process | 1984 thread, 30
        17728 process | 20896 thread, 30
        17728 process | 45784 thread, 30
        17728 process | 8168 thread, 30
        17728 process | 44652 thread, 30
        17728 process | 38000 thread, 30
        17728 process | 40232 thread, 30
        
        10.620046138763428
    • Multi processing (약 3.5초)
      • import time
        import os
        import threading
        from concurrent.futures import ProcessPoolExecutor
        
        nums = [30] * 100
        
        
        def cpu_bound_func(num):
            print(f"{os.getpid()} process | {threading.get_ident()} thread, {num}")
            numbers = range(1, num)
            total = 1
            for i in numbers:
                for j in numbers:
                    for k in numbers:
                        total *= i * j * k
            return total
        
        
        def main():
            executor = ProcessPoolExecutor(max_workers=10)
            results = list(executor.map(cpu_bound_func, nums))
        
        
        if __name__ == "__main__":
            start = time.time()
            main()
            end = time.time()
            print(end - start)
            
        -->
        29884 process | 44836 thread, 30
        20012 process | 26932 thread, 30
        25904 process | 31908 thread, 30
        34984 process | 29528 thread, 30
        ...
        19316 process | 36244 thread, 30
        33032 process | 14816 thread, 30
        25904 process | 31908 thread, 30
        37712 process | 7116 thread, 30
        20012 process | 26932 thread, 30
        46344 process | 25716 thread, 30
        26324 process | 16976 thread, 30
        3.5594847202301025
    • 정리
      • 시간 : 기본 >> 멀티 스레딩 >> 멀티 프로세싱
      • 멀티 스레딩의 경우 시간 차이가 거의 없는데 이것은 이제 파이썬의 GIL로 인하여 한번에 하나의 스레드만 실행되는 문제 때문입니다. 따라서 멀티 스레딩일 경우는 멀티 프로세싱과 달리 병렬적으로 실행되지가 않고 동시적으로만 작동합니다. 따라서 기본의 경우와 시간 차이가 거의 존재하지 않게 됩니다.