Share
Sign In
Lighthouse Dev
파이썬의 동시성 관리 : 코루틴(Corutine) 上
L
Lighthouse
😍
1
0. 들어가기 앞서
지난 시간에는 파이썬의 GIL 제약과 그 제약으로 인해 “찐”효율을 내지 못하는 멀티 스레드 프로그래밍에 대해서 알아보았다.
이번 시간에는 파이썬 비동기의 핵심 키워드 네이티브 코루틴인 Asyncio, async, await를 이해하기 전 이것들의 근간이라 불리는 코루틴(Corutine)에 대해서 알아보자.
들어가기 앞서, 복습 차원에서 지난 시간 내용을 간략하게 정리하자
1. 제네레이터
1.1. 제네레이터
제네레이터는 쉽게 말해서, 여러개의 데이터를 미리 만들어 놓지 않고 필요한 때마다 즉석에서 하나씩 만들어낼 수 있는 객체를 의미한다.
일반적인 함수와 달리 상태를 유지할 수 있다.
즉, 제네레이터는 yield 표현식을 사용하여 값을 반환하고, 다음 호출 시 마지막으로 실행된 yield 표현식 이후부터 실행을 재개한다. 일반함수는 return을 만나면 실행이 끝나버린다. 하지만 제네레이터는 yield 구문에서 “일시정지”의 상태로 값을 외부로 내보낸다. 그 이후에 필요할 때 다시 실행 흐름을 이어나갈 수 있다. 함수 내부에서 사용된 지역 변수등이 메모리에 그대로 유지되어 있기 때문이다.
아래 코드를 살펴보자.
import time def return_abc(): alphabet_list = [] for alphabet in "ABC": time.sleep(1) alphabet_list.append(alphabet) return alphabet_list
함수 return_abc()는, 알파벳을 1초마다 하나하나 리스트에 적재하는 코드이다.
print를 찍어본다면 어떻게 될까?
print(return_abc()) ## 출력값 ['A', 'B', 'C']
너무 당연한 결과이다.
그렇다면 해당 함수를 for loop에 돌려보면 어떻게 될까?
for alphabet in return_abc(): print(alphabet) ## 출력값 ...3초 후 A B C
이 결과 역시 너무나도 쉽게 예상할 수 있다.
여기서 3초라는 시간을 잘 기억해주길 바란다.
위에서 제네레이터는 데이터를 미리 만들어두지 않고, 필요할 때마다 하나씩 만들어내는 객체를 뜻한다 했다.
이것도 코드를 통해 알아보자.
import time def yield_abc(): for alphabet in "ABC": time.sleep(1) yield alphabet
뭔가 위의 코드랑 별로 달라진게 없어보인다.
print를 찍어보자.
print(yield_abc()) ## 출력값 <generator object yield_abc at 0x104fa5620>
!! 예상했던 것과는 달리 제네레이터가 출력 되었다 !!
위에서 언급했던 것처럼 제네레이터는 “필요할 때마다” “하나씩” 만들어 낸다고 했으니, 한번 for문을 통해서 “하나씩” 값을 받아와 보자.
for alphabet in yield_abc(): print(alphabet) ## 출력값 ...1초 후 A ...1초 후 B ...1초 후 C
return_abc() 함수와 비교하였을때, 시간상으로는 똑같이 3초+@가 소요 되었다. 하지만 값을 수령하는 방식에서 차이가 발생했다.
return_abc는 한번에 전부.
yield_abc는 한개씩.
예제코드처럼, ABC 3개의 알파벳이 아닌, 수천 수만개의 알파벳이라면?
yield_abc는 수천~수만초의 시간이 흐르기 전에 값을 계속해서 수령해서 프로세스가 진행 될 테고,
(A 수령 → 작업 → B수령 → 작업 → C수령 → 작업 ………)
return_abc는 수천~수만초의 시간이 흐를때 동안 아무것도 하지 못한체, 하염없이 return_abc의 리턴값만 기다릴 것이다.
(대기…………………………..→ 총 수령 → 총 작업)
또, 메모리 효율 측면에서도 말할게 생기는데 return 키워드를 사용할 때는 모든 결과 값을 메모리에 올려놓아야 하는 반면에, yield 키워드를 사용할 때는 결과 값을 하나하나 메모리에 올려놓기 때문에 메모리 측면에서도 효율 적이다.
( 특히 한번에 메모리에 가득 올리기에 부담이 가는 대용량 파일을 읽을때 효율적일 것 같다 !)
그러한 특성들 때문에 제네레이터는 흔히 게으른 반복자(lazy iterator)라고도 불린다. 이러한 게으른 특성을 잘 활용한다면 효율적인 작업이 가능하다 !
얼떨결에, 장점까지 말해버렸다.
1.2. yield ?
제네레이터도 이해하기 힘든데, 생소한 yield가 등장해버렸다. 어디서 본적이 있지만, 스쳐지나간 적이 많은 키워드라고 생각한다.
보통의 함수는 호출되면 그 내부의 모든 코드를 실행하고 값을 반환한 후 종료되지만, yield를 사용하면 함수는 값을 반환한 후에도 종료되지 않고 일시적으로 멈춘다.
그리고 다음 번에 다시 호출되면 멈췄던 그 지점 코드부터 실행을 재개한다.
⚠️❗⚠️❗⚠️❗⚠️ 선생님 이해가 안돼요❗⚠️❗⚠️❗⚠️❗⚠️❗
이것만 기억하자 !!
yield 키워드는, 코루틴에서 값의 “전달(함수 재개)”과 “반환(함수 중지)”을 모두 담당한다.
값 전달 (input) - 함수 재개
yield를 통해 값을 할당 받는다.
중단되었던 함수라면, 중단됐던 yield의 다음 코드부터 재개 된다.
이때, input값은 일전에 중단되었던 yield 부분에서 할당된다.
값 반환 (output)
yield를 통해 값을 반환한다.
함수가 진행되면서, yield를 만나면 함수를 중단하고 값을 반환한다.
즉 yield는 함수가 일시중지되는 지점을 결정하고, 다시 재개되는 지점을 결정하는 중요한 역할을 한다.
2. 코루틴 (Corutine)
2.1. 코루틴 이란?
코루틴(Corutine)이란, Cooperative + Routine의 의미로, “상호 협력하는 루틴”이라고 할 수 있다.
파이썬의 asyncio / async와 await 키워드가 바로 코루틴을 기반을 둔 비동기 프로그래밍 기법이다.
async - 코루틴 함수 선언.
await - 대기하면서, 다른 루틴이 실행될 수 있도록.
서브루틴과 달리 현재 상태값을 저장하고 메인루틴으로 돌아간 뒤 나중에 호출하게 되면 저장했던 상태를 꺼내서 다음 상태를 진행할 수 있는 함수(루틴)
❓상호 협력
직관적인 이해를 위해, 코드와 그림을 통해 이해해보자 !
def normal_hello(time: str): greeting = "good " return greeting + time + ' Lighthouse' def start_normal_greeting(): print(normal_hello("morning")) print(normal_hello("afternoon")) print(normal_hello("evening")) start_normal_greeting()
## 출력값 good morning Lighthouse good afternoon Lighthouse good evening Lighthouse
팀원들에게 아침, 점심, 저녁 인사를 하는 너무나도 간단한 코드이다.
start_normal_greeting함수 안에서 normal_hello 함수를 호출하고, normal_hello함수가 끝남과 동시에,
normal_hello 함수에 들어있던 모든 것은 사라진다.
이를 보통 메인 루틴에서 서브 루틴을 호출하고, 서브루틴은 종료된다 라고 얘기한다.
아래 그림으로 간단하게 살펴보자.
2.1.1. 일반적인 루틴의 플로우
그림과 같이, 서브루틴은 메인루틴에 의해 호출 및 종료가 결정이되는 **“종속”**관계라고 할 수 있다.
하지만, 코루틴은 방식이 다르다.
위처럼 메인 루틴과 서브 루틴이 **“종속”**적인 관계가 아니라, 서로 **“대등”**한 관계이며, 특정 시점에 상대방의 코드를 실행한다.
아래 예시 코드를 보자.
def coroutine_hello(): greeting = "good " while True: text = (yield greeting) greeting += text def starting_corutine_greeting(): cr = coroutine_hello() next(cr) print(cr.send("morning")) print(cr.send("afternoon")) print(cr.send("evening")) starting_corutine_greeting()
뭔가 코드의 형태도 비슷하고, 출력 값 또한 일치한다 ..! 또 눈에 띄는것이, 아까 설명했던 yield가 등장한다 !
그럼. 해당 코드의 출력 값은 어떻게 될까?
출력값
good morning
good morningafternoon
good morningafternoonevening
원하던 바가 아니다.
그럼 팀원들에게 제대로된 인사를 하기 위해서는 코드를 어떻게 수정해야할까?
def coroutine_hello(): greeting = "good " text = yield while True: text = yield greeting + text + ' Lighthouse' def starting_corutine_greeting(): cr = coroutine_hello() next(cr) print(cr.send("morning")) print(cr.send("afternoon")) print(cr.send("evening")) starting_corutine_greeting()
## 출력값 good morning Lighthouse good afternoon Lighthouse good evening Lighthouse
메인 - 서브 루틴에서의 종속적인 관계와 달리, 코루틴에서는 coroutine_hello 함수가 실행-종료를 반복되는 것이 아니라, 계속해서 “대기”를 하게된다.
print(cr.send("morning")) print(cr.send("afternoon")) print(cr.send("evening"))
print문에서 3번 호출된다 해서
3번 실행 - 3번종료가 아닌,
최초 실행 - 대기 (작업수행) - 종료 라는 것이다 !
다시말해서, 함수의 호출과 종료가 한큐에 일어나는 서브루틴과 달리, 코루틴에서는 함수가 종료될 것 같은 시기에 대기를 하게되니, 다시 호출(사실은 재개)이 된다 하더라도, 변수값이 계속 유지되기 때문이다.
그림으로 보면 다음과 같다.
2.1.2. 코루틴의 플로우
이처럼 코루틴은 함수가 종료되지 않은 상태에서 메인 루틴의 코드를 실행하고, 다시 돌아와 코루틴의 코드를 실행한다. 함수의 코드를 실행하는 지점을 진입점(entry point)라고 하는데, 코루틴은 진입점이 여러개다. (yield) 이런 점에서 코루틴은 일반 루틴에 비해 메인 루틴과의 종속 관계가 아닌 대등한 관계이며, 그래서 상호 협력하는 관계라고 칭한다.
마치며
사실 이번 발표는, 처음 코루틴 발표를 계획했을 때의 30%분량도 채우지 못한 것 같다. 원래 목표는 코루틴에 대한 간단한 설명과 더불어 지난 시간에 발표한 멀티스레딩 프로그래밍과 예시코드로 직접적인 속도비교 및 분석 까지가 목표 였는데, 발표를 위한 공부를 이어가다보니 기반을 닦아 놓지 않으면 이도저도 안될 것 같아서 기초적인 원리에 중점을 둔 발표라고 생각한다.
이번 발표를 간단히 정리해보자면 딱 “코루틴의 개요” 정도일 것 같다.
step1인 개요를 지났으니, 다음 시간에는 좀 더 딥다이브 하게 코루틴에 대해 살펴보고, 코루틴의 발전과정 / async/await로의 진화 과정을 설명 해볼까 한다.
출처
Kp
Subscribe to 'kpmg-lighthouse'
Welcome to 'kpmg-lighthouse'!
By subscribing to my site, you'll be the first to receive notifications and emails about the latest updates, including new posts.
Join SlashPage and subscribe to 'kpmg-lighthouse'!
Subscribe
😍
1
Lighthouse_JSH
Airflow 트러블슈팅 공유
안녕하세요! 오늘은 제가 Airflow를 사용하면서 겪었던 몇 가지 트러블슈팅 경험을 공유하고자 합니다. 제 경험이 여러분의 Airflow 작업에 도움이 되기를 바랍니다. 디렉토리 구조 제가 구성한 Airflow의 디렉토리 구조는 다음과 같습니다. 추가한 디렉토리에서 import 시 에러 발생 새로운 디렉토리인 config와 db에서 import하려고 시도했을 때 import 오류가 발생했습니다. 이는 프로젝트 경로를 path에 추가하여 해결했습니다. path를 추가하며 운영 환경과 개발 환경의 경로가 다르기 때문에 다른 문제가 발생했는데 이는 아래에서 다시 설명드리겠습니다. plugins와 dags/utils 처음에는 azure_conn과 slack_message 두 개의 스크립트를 plugins 디렉토리에 저장했습니다. 하지만 GCP Composer 서비스에서는 dags 폴더 하위에 폴더를 생성하도록 권장하기 때문에 코드 재사용성을 위해 두 스크립트를 dags/utils 디렉토리로 이동했습니다. plugins 디렉토리는 Airflow 시스템의 기본 기능 확장에 사용합니다. 출처: https://cloud.google.com/composer/docs/how-to/using/installing-python-dependencies?hl=ko#use_packages_that_depend_on_shared_object_libraries API 호출 에러 사례 스케줄러나 웹 UI를 통해 DAG를 실행할 때 Negsignal.SIGSEGV 오류가 발생하여 requests 호출이 중단되었습니다. 로컬에서 테스트 실행해 보니 문제 없이 동작했지만, 이 오류는 이미 GitHub 디스커션에 등록되어 있었습니다. 출처: https://github.com/apache/airflow/discussions/24463#discussioncomment-4404542 원인은 명확하게 밝혀지지 않았지만, macOS에서는 프록시 정보를 가져오는 시스템 호출이 특정 조건 하에서 예상치 못한 방식으로 작동하여 메모리 접근 오류인 SIGSEGV(세그먼테이션 폴트)를 발생시키는 것으로 추측됩니다. 해결방법은 dag에 아래의 코드를 추가해 프록시 설정을 초기화하였습니다. 실행 환경 차이로 인한 코드 불일치 위 에러들로 인해 실행 환경과 개발환경 간 코드가 불일치했습니다. 이를 해결하고자 config/airflow_config/config.json을 두어 실행 환경을 읽어 환경에 따라 같은 코드로 다르게 동작하도록 수정했습니다. Connections 등록 후 PW를 까먹은 경우
김원준
파이썬의 동시성 관리 : GIL 과 멀티 스레딩
0. 들어가며 저번 시간에는 동시성. 즉, 제어권에 대한 Blocking/NonBolocking. 또 작업의 순서를 논하는 Sync/Async에 대해 간단하게 살펴보았다. 그렇다면 이번 시간에는 다음 순서로 파이썬의 GIL 및 스레드 환경에 대해 살펴보자. 0-1. 들어가기 앞서 ! 오늘 발표를 조금 더 잘 이해할 수 있도록 하기 위해, 몇가지 용어에 대한 설명을 준비하였다. 메모리란 ? 메모리는 컴퓨터가 프로그램과 데이터를 저장하고 처리하는 데 사용되는 저장 공간이다. 프로그램의 실행 중에 생성되는 데이터는 물리적인 메모리에 저장 되며(주로 RAM), 이 데이터는 스레드가 실행되면서 읽고 쓰게 된다. 프로세스란 ? 실행 중에 있는 프로그램을 의미한다. 작업(Task)과 같은 의미로 쓰인다. 프로세스는 최소 하나의 스레드를 가지는데, 실제로 작업(Task)이 스레드 단위로 동작한다. 스레드란 ? 스레드는 프로그램 내에서 실행되는 흐름의 단위. 일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램의 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다. 이것이 바로 멀티스레딩이다. 프로세스 구조 프로세스의 데이터와 명령어가 있는 영역은 Code(Text), Data, Stack, Heap이다. 💡각 프로세스는 별도의 공간(독립된 메모리)에서 실행되고 프로세스끼리는 자원의 공유를 하지 않는다. 그렇다보니, 프로세스 간의 자원을 공유하기 위해서는 별도의 통신이 필요하다. 스레드 구조 스레드는 Stack만 따로 할당받고, Code(Text), Data, Heap 영역은 프로세스의 자원을 공유한다. 💡자원을 공유하다보니, 시스템의 자원과 처리 비용이 멀티 프로세싱에 비해 적다. (통신의 부담X) 하지만 자원을 공유하고 있다 보니, 멀티 스레딩 환경에서 동기화의 문제가 발생할 수 있다.
❤️
2
Lighthouse
TDD(Test-Driven Development)
안녕하세요, 오늘은 TDD(Test-Driven Development)에 대해 이야기하려 합니다. TDD 소프트웨어 개발 방법론 중 하나로, 개발 과정에서 테스트를 우선하여 작성하고 이를 통과시키는 것에 초점을 두는 방법론입니다. 코드를 작성하기 이전에 테스트 케이스를 작성하고, 이를 통과하기 위한 그에 맞는 기능을 작성하는 작업을 반복하여 개발합니다. TDD의 장점과 한계 TDD의 장점: 품질 향상: 개발자는 코드의 동작을 확실히 이해하고 테스트 케이스를 통해 코드의 정확성을 검증할 수 있어, 더 견고하고 안정적인 코드를 작성할 수 있습니다. 리팩토링 지원: 코드를 작은 단위로 분리하고 테스트 가능한 형태로 작성하여 코드의 구조와 설계를 개선하기 쉽게 합니다. 빠른 피드백: 작은 단위의 테스트를 빠르게 실행하고 결과를 확인하여 버그를 빠르게 발견하고 수정할 수 있으며, 코드 변경에 대한 피드백을 신속하게 받을 수 있습니다. 협업 강화: 테스트 코드가 개발 프로세스의 일부로 문서화되어 있어 다른 개발자들과의 협업을 용이하게 하며, 코드의 동작을 이해하고 검증하는 데 도움이 됩니다. TDD의 한계: 시간과 노력: 테스트 코드를 작성하기 위해 추가적인 시간과 노력이 필요하며, 개발 시간이 늘어날 수 있습니다. 설계에 대한 이해: 테스트 케이스를 작성하기 위해 개발자는 코드의 설계에 대한 깊은 이해가 필요하며, 이를 갖추지 않으면 테스트 케이스가 미흡해질 수 있습니다. 복잡한 시나리오의 테스트: 복잡한 시나리오나 UI, 성능 테스트 등은 TDD로 테스트하기 어려울 수 있습니다. 변동이 많은 요구사항: 요구사항이 자주 변경되는 경우, 테스트 케이스를 계속 수정해야 하는 상황이 발생할 수 있습니다. 출처: https://semaphoreci.com/wp-content/uploads/2022/02/tdd-vs-waterfall.webp 이러한 장점과 한계로 인해 TDD는 초기에는 추가 비용이 들 수 있지만, 시간이 지남에 따라 비용을 절감할 수 있는 특징을 지녔습니다. TDD의 사이클 Lifecycle of the Test-Driven Development method 출처: https://ko.wikipedia.org/wiki/테스트_주도_개발 Write Test: 기능을 검증할 테스트 케이스를 작성