1. 동기(sync) vs 비동기(async)
동기의 또 다른 이름은 '순차적인 실행'이라고 정의할 수 있다. 예를들어 텍스트 데이터를 활용한 머신러닝 모델링을 한다고 해보자. [1. 텍스트 데이터 추출 -> 2. 텍스트 데이터를 숫자로 전처리 3. 모델에 입력으로 넣기 -> 4.모델 성능측정] 이 4가지 단계는 반드시 이전 단계가 끝나야만 다음 단계가 실행하도록 하는 순차적인 실행을 지켜야 한다. 다시 말해 2번의 로직이 끝나지 않았는데 3단계를 수행 할수 없다. 바로 이렇게 반드시 순차적인 실행 흐름을 지켜야 하는 것들 동기(synchronous)실행 이라고 부른다.
그러면 비동기는 무엇일까? 순차적으로 실행되지 않아도 되는 것 이말은 곧 이전 단계가 끝나지 않더라도 다음 단계를 실행할 수 있다는 것이다. 간단하게 그림으로 표시하자면 아래와 같다!

2. 동시성(Concurrency) vs 병렬성(Parallelism)
동시성은 Task들이 빠르게 전환하면서 실행되어 동시에 실행 되는 것처럼 보이는 것이다. 병렬성은 물리적인 시간에 작업을 동시에 수행하는 것이다. 동시성과는 다르게 병렬성은 여러 작업들을 코어, 프로세스, 컴퓨터등으로 동시에 수행할 수 있으며 꼭 멀티코어 한 개 이상의 스레드가 동시에 수행하는 것에만 한정하는 것은 아니다.
그리고 동시성은 논리적인(Logical) 개념이다. 여기서 '논리적이다'라고 하는 것은 물리적인 것에 구애 받지 않고 소프트웨어로 구현 할 수 있다는 것이다. 즉 CPU Core가 몇 개 이건, 쓰레드가 몇 개이던 동시성 구현이 가능하다.
병렬성은 동시성과 달리 물리적인(Physical)개념 이다. 즉 여러 개 장착되어 있는 물리적인 장치를 단위로 해서 병렬적으로 실행되는 것이다. 이 물리적인 장치의 예로는 CPU Core 개수가 될 수도 있고, 쓰레드 개수가 될 수 있다.

그래서 병렬성을 구현하기 위해서는 무조건 멀티 코어 또는 멀티 쓰레드라는 물리적인 장치가 전제되어야 한다. 그래서 병렬성을 물리적인(Physical) 개념이라고 하며 자연스레 물리적인 장치에 구애받는다라고 할 수 있다. 따라서 병렬성을 구현하기 위해서는 멀티 프로세싱과 멀티 쓰레딩이 활용된다.

3. Python으로 병렬성(Parallelism) 구현하기
간단하게 코드 몇가지를 가져왔다. 먼저 멀티 쓰레드부터 알아보자
import os
import threading
import time
from concurrent.futures import ThreadPoolExecutor
def calculate(n):
print(f"{os.getpid()} process | {threading.get_ident()} thread | n: {n}")
total = 0
for i in range(n):
for j in range(n):
for k in range(n):
total += i * j * k
return total
def main(nums):
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(calculate, nums))
print(results)
if __name__ == "__main__":
start = time.time()
main([300] * 10)
print("elapsed-time:", time.time() - start)
총 9.1초가 걸렸다. 그리고 출력 내용의 프로세스 ID와 쓰레드 ID를 보면 1개의 프로세스 내 1개의 쓰레드만 실행 된 것을 볼 수 있다.

공식문서를 참고해서 멀티 프로세싱에 대한 코드를 보자.
import os
import threading
import time
from concurrent.futures import ProcessPoolExecutor
def calculate(n):
print(f"{os.getpid()} process | {threading.get_ident()} thread | n: {n}")
total = 0
for i in range(n):
for j in range(n):
for k in range(n):
total += i * j * k
return total
def main(nums):
with ProcessPoolExecutor(max_workers=10) as executor:
results = list(executor.map(calculate, nums))
print(results)
if __name__ == "__main__":
start = time.time()
main([300] * 10)
print("elapsed-time:", time.time() - start) # 1.4초
총 걸린 시간이 1.4초로 코드 실행 시간이 압도적으로 줄었음을 볼 수 있다.

이상입니다!
관련하여 궁금한 사항 있으면 댓글 부탁드립니다.
출처