동시성, 병렬성
동시성: 한 컴퓨터 시스템이 여러 작업을 번갈아가며 실행하여 논리적으로 동시에 수행되는 것처럼 보이는 능력
ex) 단일 CPU에서 멀티스레딩을 사용하여 여러 작업을 처리
스레드는 프로세스 내에서 제어영역을 담당하는 하는 부분으로, 프로세스의 Code, Data, Heap 영역을 서로 공유한다.
가벼운 문맥전환이 가능하고 GIL을 통해서 스레드 동기화를 수행한다.
스레드 동기화: 여러 스레드가 동시에 데이터나 자원에 접근할 때 발생하는 문제 방지하는 방법
*병렬성: 여러 작업을 실제로 동시에 처리하는 능력
ex) 여러 CPU 코어 또는 프로세서(멀티프로세싱)를 사용하여 각 작업을 물리적으로 동시에 처리
바운드
I/O 바운드: CPU보다 입출력 작업에서 더 많은 시간이 소요되는 작업으로, 실행속도가 I/O에 의해 제한된다.
ex) 파일 다운 프로그램에선 인터넷에서 파일을 다운받는 속도가 프로그램 실행속도를 결정한다.
CPU 바운드: 입출력 작업보다 CPU 작업에서 더 많은 시간이 소요되는 작업으로, CPU 속도에 의해 제한된다.
ex) 복잡한 계산 또는 데이터 처리를 하는 작업
비동기 프로그래밍
동기: 순차적으로 코드가 작성된 순서 그대로 실행되며 하나의 작업이 완료될 때까지 다음 작업은 대기하는 방식
- 파이썬은 기본적으로 동기식 언어이다.
- CPU 바운드 작업에서 효과적이다. 동기식 처리가 단일 작업에 집중하여 처리가 더 빨라진다.
비동기: 작성된 순서가 아닌 동시적, 병렬적으로 작업을 실행할 수 있으며 하나의 작업이 완료될 때까지 기다리지 않고
다른 작업을 실행할 수 있다.
- 파이썬에서 asyncio 모듈, 스레딩, 멀티 프로세싱, 그 외 비동기 처리를 위한 라이브러리/프레임워크를 사용하여
비동기를 구현할 수 있다.
- 문맥 전환 오버헤드가 발생해 동기식 작업보다 느릴 수 있다.
*이때 비동기 문맥 전환 오버헤드를 멀티스레딩/멀티프로세싱에서 스레드/프로세스 간의 문맥전환과 구분
비동기 문맥 전환 오버헤드: 코루틴 간의 전환 시 발생, 이벤트 루프 관리 오버헤드
스레드/프로세스 간의 문맥 전환 오버헤드: 레지스터 상태 저장/복원, 캐시 무효화, 스케쥴링 오버헤드
*멀티스레딩과 멀티프로세싱은 동시성을 구현하는 방법이지, 비동기와 같은 개념이 아니다.
비동기는 작업의 처리방식일뿐이기 때문에 멀티스레딩(또는 멀티프로세싱)으로 동기식 작업을 구현할 수도 있다.
#코드1
import threading
import time
def task(name):
print(f"Starting task {name}")
time.sleep(2) # 동기식 작업
print(f"Task {name} completed")
threads = []
for i in range(2):
thread = threading.Thread(target=task, args=(f'Thread-{i}',))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print("All tasks completed")
각 스레드가 '순차적으로' 작업을 수행하며 여러 스레드가 '동시에' 실행된다.
마찬가지로 멀티스레딩을 통하여 비동기 작업을 구현할 수 있지만, 보통 asyncio 같은 비동기 라이브러리를 사용한다.
asyncio (코루틴, 이벤트루프, 테스크, 퓨처)
asyncio의 구성요소로는 코루틴, 이벤트루프, 테스크, 퓨처가 있다.
▶코루틴: Co + Routine, 상호협력하는 루틴으로서 메인루틴에 종속되는 서브루틴과 달리 메인루틴의 흐름제어를 돕는
동등한 지위의 함수이다.
- 제너레이터와 같이 특정 시점에서 실행을 일시 중단하고 나중에 해당 지점부터 실행을 재개하는 방식
- 다만 제너레이터는 호출시 함수가 실행되면서 제너레이터 객체를 반환하지만
코루틴은 함수가 실행되지는 않고 코루틴 객체만 반환한다.
- async 키워드로 코루틴을 정의하고 await 키워드를 사용하여 비동기 함수를 호출한다(generator의 yield와 유사)
▶퓨처: 아직 완료되지 않은 어떠한 작업의 실행 상태 및 결과를 저장하는 객체
- 실행 상태: ①Pending ②Cancelled ③Finished 세 가지 상태 중 하나를 가지며 ②,③은 작업의 완료를 나타냄
- 실행 결과: 작업의 결과값 또는 작업 중 발생한 예외 객체
- add_done_callback() 메소드: 해당 퓨처 객체가 완료될 때 호출될 함수를 등록
▶테스크: 퓨처를 상속하는 클래스로, 퓨처와 유사하지만 테스크 객체는 어떠한 작업의 실행을 개시하는 역할도 한다.
- 코루틴 객체를 매개변수로 받으면서 생성된다.
task = asyncio.create_task(my_coroutine())
- 해당 코루틴이 테스크로서 이벤트 루프를 돌도록 예약을 거는데, 이때 __step() 메소드를 사용한다.
__step() 메소드는 코루틴을 실행하고 예외를 처리하며 결과를 저장한다.
- 처음 실행된 코루틴은 await 키워드를 통해 또 다른 코루틴을 부르며 코루틴 체인을 만들 수 있다.
이러한 체인 내에서 sleep이나 코루틴을 await하는 코드를 만나면 이벤트 루프는 자신에게 예약된 테스크 중
우선순위가 높은 것을 선택하여 실행하며, sleep이나 await된 코드는 다시 실행될 수 있는 상태가 되면 이벤트 루프에
예약을 걸어둔다. 테스트 객체가 받은 코루틴의 모든 yield 키워드가 소진되면 이벤트 루프가 종료된다.(아래참고)
▶이벤트루프: 코루틴, 테스크 등 대기 중인 작업을 예약했다가, 원할 때 작업을 재개할 수 있는 루프
- asyncio.run() 함수가 현재의 스레드에 asyncio.new_event_loop()를 통해 이벤트 루프를 생성하면
매개변수로 넘겨받은 코루틴(예시의 main())을 loop.create_task(main)으로 테스트 객체를 만들어
이벤트 루프에 예약한다.
- loop.run_until_complete(main)을 통해 이벤트 루프가 시작되고
테스트 객체가 받은 코루틴의 모든 yield 키워드가 소진되면 loop.close()를 통해 루프를 닫고 asyncio.run()이 종료된다.
- 코드2처럼, asyncio.create_task()함수를 사용해 테스크 객체를 직접 만들어 실행할 수도 있다.
*asyncio.run()함수가 이벤트 루프를 제어하는 복잡한 작업을 자동으로 처리해 준다.
#코드2
import asyncio
async def my_coroutine():
print("Coroutine started")
await asyncio.sleep(1)
print("Coroutine resumed after 1 second")
#async로 선언한 main함수를 엔트리 포인트로 가지는 asyncio.run()함수
async def main():
task = asyncio.create_task(my_coroutine())
await task
asyncio.run(main())
코드 작동 순서
- syncio.run(main())호출
- main 코루틴이 테스크 객체화 되어 이벤트 루프에 등록
- main 코루틴 실행
- my_coroutine()이 asyncio.create_task에 의해 테스크 객체화 되어 이벤트 루프에 등록
- await task 실행, my_coroutine() 코루틴이 완료될 때까지 main 코루틴 대기
- my_coroutine() 코루틴 실행
- "Coroutine started" 출력 후 await asyncio.sleep(1) 실행, 1초 동안 비동기적 대기
- 이벤트 루프에 다른 대기 작업이 있었으면 실행, 없으므로 대기
- 1초 후 my_coroutine 코루틴 재실행
- "Couroutine resumed after 1 second" 출력 후 my_coroutine 테스크 종료
- main 코루틴 재실행, 이하 코드 없으므로 main 테스크 종료
- asyncio.run(main()) 함수가 이벤트 루프 해제하고 종료
멀티스레딩 vs 코루틴
동시성을 위한 비동기를 처리하는 방법에는 멀티스레딩도 있지 않는가?
▶스레드 vs 코루틴
스레드로 비동기를 처리할 시 스레드 단위로 task가 수행되기 때문에 실행될 task가 바뀔 시 문맥 전환이 발생한다.
코루틴은 문맥전환에 따른 비용 없이 동시성을 보장할 수 있다.
GIL을 효율적으로 사용하므로 CPU 바운드 작업에서도 효율적으로 작동한다.
참고 사이트
https://it-eldorado.tistory.com/159
'Lauguage > Python' 카테고리의 다른 글
[Python] 상속과 다형성 (0) | 2024.06.16 |
---|---|
[Python] 캡슐화 (0) | 2024.06.15 |
[Python] GIL (Global Interpreter Lock) (0) | 2024.06.11 |
[Python] [혼자 공부하는 파이썬] Chapter 8. 클래스 (0) | 2024.06.10 |
[Python] [혼자 공부하는 파이썬] Chapter 6-7 예외처리, 모듈 (1) | 2024.06.08 |