그린렛
그린렛은 Python 프로그램 하나 안에서 실행 흐름을 여러 갈래로 갈라 두는 물건입니다. 갈래 하나하나를 그린렛이라고 부릅니다. 한 번에 하나만 돕니다. 다른 갈래로 넘어가는 시점은 프로그램을 짠 사람이 직접 정합니다.
상세
greenlet 공식 문서는 그린렛을 한 프로세스 안에서 순차적으로 도는 동시성 프로그래밍용
코루틴이라고 적습니다. 그린렛 하나는 greenlet 클래스의 객체로 나타납니다. 문서가 붙인 설명은
두 갈래입니다. 하나는 작은 독립 유사 스레드라는 것이고, 다른 하나는 프레임이 쌓인 작은 스택
하나로 생각하라는 것입니다.
전환
그린렛 사이를 건너뛰는 것을 전환이라고 부릅니다. 문서가 쓰는 말은 switching 입니다.
generator.send(val) 과 비슷하게, 전환할 때 객체를 함께 넘길 수 있습니다.
API(Application Programming Interface, 응용 프로그램 인터페이스) 문서에 적힌 시그니처는
이렇습니다.
switch(*args, **kwargs)
전환이 일어나는 자리는 둘입니다. 어떤 그린렛의 switch 메서드가 불리면 실행이 그 그린렛으로
건너뜁니다. 그린렛이 죽으면 실행이 부모 그린렛으로 건너뜁니다. 전환하는 동안 대상 그린렛에게
객체나 예외 하나가 보내집니다. 그린렛 사이에 정보를 넘기는 수단으로 이것을 씁니다.
x = g.switch(y) 는 g 에게 객체 y 를 보냅니다. 그리고 나중에 아무 그린렛이 되돌려준
객체를 x 에 넣습니다.
공식 문서가 싣는 예제가 이 왕복을 그대로 보여 줍니다.
>>> from greenlet import greenlet
>>> def test1(x, y):
... z = gr2.switch(x + y)
... print(z)
>>> def test2(u):
... print(u)
... gr1.switch(42)
>>> gr1 = greenlet(test1)
>>> gr2 = greenlet(test2)
>>> gr1.switch("hello", " world")
hello world
42
sequenceDiagram
participant 주 as 주 그린렛
participant gr1
participant gr2
주->>gr1: switch 로 두 문자열을 넘긴다
gr1->>gr2: switch 로 합친 문자열을 넘긴다
Note over gr2: hello world 를 찍는다
gr2->>gr1: switch 로 42 를 넘긴다
Note over gr1: 42 를 찍는다
스레드와 갈리는 자리
그린렛은 협력적이고 순차적입니다. 한 그린렛이 도는 동안에는 다른 어떤 그린렛도 돌 수 없습니다. 실행이 그린렛 사이를 언제 옮겨 가는지는 프로그래머가 완전히 통제합니다. 문서는 스레드를 그 반대편에 놓습니다. 스레드는 이론상 선점적이고 병렬입니다. 여러 스레드가 동시에 일을 처리할 수 있다는 뜻입니다.
콜스택에 C 함수가 있어도 된다
그린렛은 C 라이브러리와 맞물려 동작합니다. 콜스택에 C 함수가 올라가 있는 상태에서도 전환할 수 있다고 문서는 적습니다. 이 성질이 뒤에 오는 재진입성 이야기의 뿌리입니다.
어디서 나왔나
greenlet 패키지는 Stackless 에서 갈라져 나온 것입니다. Stackless 는 tasklet 이라는 마이크로 스레드를 지원하는 CPython 의 한 판입니다.
무엇에 쓰나
문서가 드는 쓸모는 하나로 모입니다. 겉보기에 비동기인 작업을 단순한 동기 방식으로 바꿔 쓰는 것입니다. 그린렛만 따로 쓸 수도 있지만, 더 높은 수준의 추상과 비동기 입출력을 얻으려고 gevent 같은 프레임워크와 함께 쓰는 일이 잦다고 적혀 있습니다.
포기한 것
선점
그린렛은 실행 시간을 나눠 갖는 일을 스케줄러에게 맡기지 않습니다. 협력적이고 순차적이라는 말이 그 뜻입니다. 얻은 것은 통제권입니다. 실행이 언제 그린렛 사이를 옮겨 가는지를 프로그래머가 완전히 쥡니다. 내준 것은 밀어낼 방법입니다. 한 그린렛이 도는 동안 다른 어떤 그린렛도 돌 수 없으므로, 제어를 내놓지 않는 그린렛을 강제로 세울 장치가 없습니다.
여러 코어
그린렛은 일을 여러 코어에 흩지 않습니다. gevent 문서는 그린렛이 전부 같은 운영체제 스레드 하나 안에서 돌고 협력적으로 스케줄된다고 적습니다. 여러 코어를 쓰려면 프로세스를 여러 개 띄우게 됩니다. Locust 문서가 실제로는 코어마다 Locust 프로세스를 하나씩 돌려야 한다고 적는 자리가 그것입니다. Python 쪽 사정도 같은 방향을 가리킵니다. greenlet 문서는 GIL(Global Interpreter Lock, 전역 인터프리터 잠금)이 대체로 두 스레드가 동시에 Python 코드를 실행하는 것을 막는다고 적습니다.
스레드 경계를 넘는 전환
그린렛은 Python 스레드와 함께 쓸 수 있습니다. 그때는 스레드마다 독립된 주 그린렛이 있고 그 아래에 하위 그린렛 트리가 달립니다. 대신 서로 다른 스레드에 속한 그린렛끼리는 섞거나 전환할 수 없습니다. 트리는 스레드 경계에서 끊깁니다.
입출력을 알아보는 일
그린렛 자신은 이벤트 루프를 갖고 있지 않습니다. 소켓을 읽다 막히는 순간을 알아채고 다른 그린렛으로 넘겨주는 일을 안 합니다. greenlet 공식 문서는 그린렛을 libev 와 libuv 같은 입출력 이벤트 루프에 붙인 사례로 gevent 를 듭니다. 표준 라이브러리의 막히는 호출을 협력적으로 만드는 일도 gevent 쪽에 있습니다. gevent 는 그런 함수들의 협력 버전을 여럿 제공하고, 제삼자 모듈을 협력적으로 만드는 몽키 패치 도구를 함께 냅니다. 즉 그린렛이 내놓는 것은 전환 원시 기능 하나뿐이고, 무엇을 계기로 전환할지는 위층이 정합니다.
표준 추적과 프로파일링
스택과 프레임 전환이 같은 Python 스레드 안에서 일어납니다. 그 대가로 표준 Python 추적과
프로파일링이 그린렛과 함께 쓰이면 기대대로 동작하지 않습니다. 공식 문서는 통상적인 방법으로
그린렛 전환을 확실하게 알아채기가 어렵다고 적습니다. 그래서 greenlet 모듈에 gettrace 와
settrace 라는 함수를 따로 두었습니다. 그린렛 기반 코드의 디버깅과 추적과 프로파일링을
지원하려고 만든 자리입니다. 기존 도구를 그대로 쓰게 하는 대신 전용 손잡이를 내준 것입니다.
네이티브 함수의 재진입성
그린렛은 콜스택에 C 함수가 올라가 있어도 전환합니다. 공식 문서가 그린렛이 C 라이브러리와 잘 맞물린다고 적는 자리가 여기입니다. 대신 네이티브 C 프레임이 낀 그린렛 스택을 전환할 때는 조심하라고 공식 문서의 주의 사항 절이 적습니다. 스레드에서와 마찬가지로, 라이브러리 함수가 재진입 가능하지 않은데 둘 이상의 그린렛이 그 함수에 들어가려 하면 미묘한 문제가 생길 수 있습니다. 시그널 핸들러 함수는 나머지 프로그램이 진행하려면 반드시 반환해야 한다는 것도 같은 자리에 적혀 있습니다.
예시
gevent
gevent 는 그린렛을 써서 고수준 동기 API 를 제공하는 코루틴 기반 Python 네트워킹 라이브러리입니다. 소개 문서가 실행 단위를 그린렛에 기반한 것이라고 직접 적습니다. 왜 골랐는지도 적혀 있습니다. 겉으로는 동기 코드를 쓰면서 안에서는 입출력 이벤트 루프가 돌게 하려는 것입니다. 그린렛 전부가 같은 운영체제 스레드에서 돌고 협력적으로 스케줄됩니다.
SQLAlchemy
SQLAlchemy 의 asyncio 확장은 greenlet 라이브러리에 의존합니다. 공식 문서가 의존성으로 못
박습니다. 어느 자리에서 쓰는지는 AsyncSession.run_sync() 가 보여 줍니다. 이 메서드는 임의의
Python 함수를 그린렛 안에서 실행합니다. 그 안에서는 전통적인 동기 프로그래밍 개념이 await 를
쓰도록 번역됩니다. 설치 조건도 문서가 적어 둡니다. 그린렛이 미리 빌드된 wheel 파일을 내놓는
플랫폼이 정해져 있고, 그 밖의 플랫폼에서는 greenlet 이 기본으로 설치되지 않습니다. 플랫폼과
무관하게 greenlet 의존성이 들어가게 하려면 [asyncio] 라는 setuptools extra 를 씁니다.
Locust
Locust 는 사용자 하나하나를 각자의 그린렛 안에서 돌립니다. 왜 골랐는지는 바로 다음 문장에 있습니다. gevent 를 쓰는 이벤트 기반 구조라서 프로세스 하나가 수천 명의 동시 사용자를 다룰 수 있다는 것입니다. 문서는 분산해서 키울 수 있고 수십만 명의 동시 사용자를 지원한다고 적습니다.
운영
몽키 패치를 부르는 자리
gevent 문서가 제일 먼저 적는 손잡이는 호출 시점입니다. 몽키 패치는 프로그램 수명의 되도록 이른 시점에 해야 합니다. 가능하면 몽키 패치가 실행되는 첫 줄이어야 한다고 소개 문서가 적습니다. 모듈 문서는 더 못 박습니다. 주 모듈은 다른 어떤 import 보다 앞에, 되도록 이 코드로 시작해야 한다는 것입니다.
from gevent import monkey; monkey.patch_all()
패치는 주 스레드에서 해야 하고, 프로그램이 단일 스레드인 동안 해야 합니다. 늦게 패치하면 동작을
믿을 수 없게 되거나 심지어 오류가 날 수 있습니다. 문서가 드는 예는 어떤 모듈이 여전히 막히는
소켓을 쓰는 경우입니다. 네이티브 스레드가 이미 만들어졌거나, atexit 나 시그널 핸들러가
설치되었거나, 소켓이 이미 만들어진 뒤에 패치하면 예측할 수 없는 결과로 이어질 수 있습니다.
무엇이 켜져 있는지는 시그니처에 그대로 적혀 있습니다.
patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=True,
ssl=True, subprocess=True, sys=False, aggressive=True, Event=True,
builtins=True, signal=True, queue=True, contextvars=True, **kwargs)
기본값이 False 인 것은 sys 하나입니다. 이 함수는 모듈 안의 해당하는 다른 함수들을 전부 불러
기본 몽키 패치를 전부 수행합니다.
프로세스 하나에 코어 하나
Locust 문서는 프로세스 하나가 코어 하나에 묶인다고 적고 수치를 붙입니다. Locust 프로세스
하나는 FastHttpUser 를 쓰면 초당 약 16000 요청, HttpUser 를 쓰면 약 4000 요청을 낼 수
있습니다. 다만 특정 하드웨어와 특정 테스트 계획에서 몇 건이 나올지는 말할 수 없으니 직접 시험해
보라고 덧붙입니다. 실제로는 CPU(Central Processing Unit, 중앙처리장치) 코어마다 Locust
프로세스를 하나씩 돌려야 한다는 것이 문서의 권고입니다. 부하 생성기의 CPU 가 과부하되지 않은
한 FastHttpUser 의 응답 시간은 HttpUser 의 것과 거의 같을 것이라고 적혀 있습니다.
실패
| 조건 | 무슨 일이 나나 |
|---|---|
| 한 그린렛이 제어를 내놓지 않는다 | 다른 그린렛이 돌 기회를 못 얻습니다 |
| CPU 를 오래 쓰는 일을 그린렛 안에서 한다 | 위와 같습니다. 입출력 위주 앱에서는 대개 문제가 아니라고 문서는 덧붙입니다 |
| 재진입 가능하지 않은 네이티브 함수에 둘 이상의 그린렛이 들어간다 | 미묘한 문제가 생길 수 있습니다. gevent 에서 libuv 의 내부 상태가 손상된 사고가 그것이었습니다 |
| 부하 생성기의 CPU 가 한계에 닿는다 | Locust 콘솔 출력에 경고가 찍힙니다 |
굶는 그린렛
gevent 문서가 못 박는 문장이 하나 있습니다. 특정 그린렛이 제어를 내놓기 전까지 다른 그린렛은 돌 기회를 얻지 못한다는 것입니다. 선점이 없으니 이것을 막아 줄 장치도 없습니다. 문서는 이것이 입출력 위주 애플리케이션에서는 대개 문제가 되지 않는다고 적습니다. 대신 CPU 를 많이 쓰는 일을 할 때는 이 점을 알고 있어야 한다고 덧붙입니다.
libuv 의 내부 상태 손상
greenlet 공식 문서의 주의 사항 절이 실제 사고 하나를 적어 두었습니다. gevent 에서 libuv 의 내부 상태가 손상된 문제입니다. 뿌리는 재진입 가능하지 않은 함수에 다시 들어간 것이었습니다. 고친 방법은 그 취약한 함수에 다시 들어가지 않게 하는 것이었습니다. 실패가 그린렛 쪽이 아니라 그린렛이 전환해 버린 C 라이브러리 쪽에서 났다는 점이 이 사고의 모양을 정합니다.
CPU 한계는 로그가 알려 준다
Locust 는 CPU 때문에 제한되고 있으면 콘솔 출력에 경고를 남깁니다. 몇 건까지 나올지는 하드웨어와 테스트 계획에 달렸으니 직접 시험해 보라고 문서가 적고, 볼 자리로 그 경고를 가리킵니다.
관련 항목
그린렛을 실제로 채택한 제품
gevent · Locust · SQLAlchemy · eventlet
실행 흐름을 나누는 다른 단위
이것이 속한 파이썬 계열
Python · CPython · Stackless Python
전환이 오가는 스택 구조
이것이 지키는 성질
협력 스케줄링 · 선점 · GIL · 동시성 · 병렬 · 스케줄링
이것에서 자주 나는 오류·장애
재진입성 · 시그널 핸들러 · libuv · libev
이것이 걸치는 입출력 경로
이벤트 루프 · 블로킹 호출 · 논블로킹 입출력 · 소켓 · 몽키 패치 · asyncio
이것과 어긋나는 개발 도구
이것이 관여하는 부하 테스트 개념
다른 이름: greenlet