오픈소스 스터디 후기와 Coroutine Scheduler 코드 분석
GDG 인천/송도와 함께 진행한 오픈소스 스터디를 참석하고나서 배운 점과 함께 후기를 남겨보려고 합니다. 그리고 마지막에 제가 발표했던 Coroutine Scheduler에서 Global Queue에 Task를 add하는 과정에 대해서 설명해보겠습니다.
진행
오픈소스 스터디는 총 4주간 진행되었습니다.
매주 일요일마다 GDG 인천/송도에서 지원해주신 장소에서 3~4명씩 발표를 하는 형식으로 진행됐습니다. (간단한 간식도 지원해주셨습니다 ㅎㅎ)
배운점과 후기
처음 스터디 장소에 모여서 각자 자기소개를 했는데 다들 뛰어나신 분들만 있는 것 같아 조금 쫄(?)리기도 했지만 오히려 더 좋다고 생각했습니다. 현업자분들이 대부분이셨기 때문에 이런 기회가 아니면 현업자분들의 발표와 분석에 대해 접해볼 기회가 없기 때문이었습니다.
4주 동안, 스터디를 진행하면서 많은 경험을 할 수 있었습니다.
첫 번째로, 오픈소스 코드를 들여다본 점 입니다. 오픈소스 코드에 대해 관심은 있어도 사실 그 방대한 코드를 들여다보기 위한 시도가 가장 큰 관문이라고 생각합니다. 아직 파악하려면 많이 멀었지만 기능 하나 정도는 이해를 한 것이 큰 성과라고 생각합니다.
두 번째로, 발표 경험입니다. 제가 오픈소스 스터디를 참여한 이유 중 하나인데, 저는 발표를 여태 많이 해보지 않아 부족하다고 생각했습니다. 발표를 꺼려했지만 이제는 발표를 해보면서 발표 경험을 쌓자는 생각이었고 스터디를 통해 발표 경험을 쌓을 수 있었습니다. (혼자 발표해보는 것 뿐만 아니라 다른 분들의 발표 스타일을 보며 또 배울 수 있었습니다.)
세 번째로, 다양한 코드를 볼 수 있었습니다. 다른 오픈 소스에서는 어떤 식으로 코드를 짰는 지, 그런 노하우를 얻을 수 있었습니다. 스케줄을 관리하기 위해 큐를 하나만 쓰거나, 여러 개를 사용하는 이유, 그리고 큐는 우선순위 큐를 사용하는 이유 등을 이야기 하면서 왜 저렇게 사용했는 가에 대해 알 수 있어서 유익한 시간이었습니다.
네 번째로, 다른 라이브러리에 대해 접해볼 수 있었습니다. reddis, query dsl 등 백엔드와 팀 프로젝트를 하면서 많이 들었던 라이브러리이지만 어떤 것인지는 잘 모르고 있었습니다. 다른 분야의 라이브러리 기능에 대해 재밌게 들을 수 있었습니다.
다섯 번째로, 현업에 대한 이야기를 들을 수 있었습니다. 발표를 하면서 겪었던 에러들과 같은 상황들을 공유하면서 ‘현업에서는 저렇게 하는구나’ 하고 들어볼 수 있었습니다.
(안드로이드를 하시는 분이 없어서 현업의 안드로이드에 대해 듣지 못한 것은 조금은 아쉬웠습니다 ㅠ)
다들 열심히 참여하시고 뛰어나신 분들이 많았기 때문에 열심히 해야겠다는 다짐을 더 단단히 할 수 있었습니다. 이런 좋은 기회가 없을 것 같아 참여했는 데 결과적으로 아주 유익한 시간이었습니다. 스터디를 열어주신 스터디장님과 GDG 인천/송도 측에 다시 한번 감사하다고 전하고 싶습니다 ㅎㅎ
발표했던 내용
Coroutine
코루틴은 Co(협력의) 접두어와 Routine(하나의 Task)가 합친 용어로써 비동기 프로그래밍을 위한 라이브러리입니다.
- 간결하고 가독성이 좋다.
왼쪽은 api 호출들을 콜백으로 엮어 처리하던 코드입니다.
하지만 이 코드를 Coroutine으로 나타내면 오른쪽처럼 아주 간결하게 표현됩니다.
비동기 프로그래밍을 아주 쉽게 사용할 수 있고 비동기 코드를 동기코드처럼 나타낼 수 있기 때문에 가독성이 뛰어난 것이 큰 장점입니다.
2.동시성
코루틴은 쓰레드 내에서 동시성의 특징을 가지고 동작합니다.
3.가볍다
코루틴은 경량화 쓰레드라고도 부릅니다.
가볍다고 하는 첫 번째 이유는, 쓰레드처럼 스택을 할당받지 않기 때문입니다.
사실 코루틴에는 스택을 할당받는 스택풀 코루틴과, 스택을 할당받지 않는 스택리스 코루틴이 있는데, 대부분은 스택리스 코루틴을 사용하기 때문에 스택을 할당받지 않아 가볍다고 합니다.
두 번째 이유는, 코루틴 간의 컨텍스트 스위칭 비용이 가볍기 때문입니다. 코루틴은 운영체제에서 관리되는 것이 아닌, 라이브러리 단계에서 관리됩니다. 그렇기 때문에 코루틴의 중단, 실행 지점은 Coroutine Context와 Continatuion 객체를 통해 관리되고, 오버 헤드가 적어 컨텍스트 스위칭 비용이 가볍게 됩니다.
그러면 이제 코드를 보겠습니다.
CoroutineScheduler 내부에는 globalCpuQueue, globalBlockingQueue, 그리고 inner class인 Worker class 안에 localQueue가 있습니다.
localQueue는 Main 쓰레드에서 동작하는 작업들이 이 큐에 담기고
globalCpuQueue에는 cpu 연산이 많은 작업들이, globalBlockingQueue에는 blocking된 작업과 I/O 작업들이 담기게 됩니다.
coroutine이 dispatch될 때 dispatch 코드를 보게 되면, 가운데에 submitToLocalQueue를 통해 먼저 localQueue에 넣어보게 됩니다. localQueue에 넣을 작업이 아니라면 반환된 값을 가지고 와서
addToGlobalQueue 함수를 실행하게 됩니다.
다시 CoroutineScheduler의 addToGlobalQueue를 보면 task가 blocking이면 globalBlockingQueue에, 나머지는 globalCpuQueue에 넣는 것을 볼 수 있습니다.
이 globalCpuQueue와 globalBlockingQueue는 GlobalQueue() 로 생성되어있고
이 GlobalQueue class를 보게 되면 LockFreeTaskQueue class를 상속받은 것을 볼 수 있습니다.
그러면 LockFreeTaskQueue class를 보겠습니다.
LockFreeTaskQueue 클래스는 singleConsumer인지 Boolean 타입을 갖고, _cur를 갖습니다.
_cur이 가장 중요한 핵심적인 요소입니다.
제가 코드를 보면서 가장 흥미로웠던 부분입니다. 이러한 코드를 처음보다 보니, 막연하게 동기화에 대한 작업은 synchronzed 같은 것을 사용하여 임계 영역을 설정하여 사용하지 않을까 싶었습니다. 하지만 LockFree 자료 구조를 만들어 사용하는 것을 알 수 있었습니다.
LockFree 자료구조는 이 자료구조에 대해 동시 접근을 허용하지만 atomic을 통한 원자적 연산을 통해 동기화를 처리하는 것을 말합니다.
_cur이 atomic으로 되어있는 이유입니다.
atomic 내부에 Core class는 LockFreeTaskQueueCore로 정의되어있는데 잠시 나중에 설명하겠습니다.
그리고 globalCpuQueue.addLast에 해당하는 addLast 함수입니다.
loop 메서드가 보이는데, 이는
의미와 같게 무한 루프를 돌리는 함수입니다.
여기서 inline 키워드에서 설명드리려고 합니다. inline은 인라이닝를 해주는 키워드입니다.
인라이닝에 대해 설명드리면, 만약 inline 키워드가 있지 않다면 loop 메서드를 실행할 때마다 action이라는 람다 함수에 대한 객체가 (컴파일 시)내부적으로 계속 생성되게 되어 낭비가 일어나게 됩니다. 하지만 이 인라이닝을 해줌으로써 action 람다 함수의 내부 코드들이 while 안에 직접적으로 삽입되어 낭비가 일어나지 않게 됩니다.
다시 addLast함수를 보게 되면, cur은 LockFreeTaskQueueCore 타입인 것을 알 수 있습니다.
when에서 실행되는 cur.addLast를 보기 위해 LockFreeTaskQueueCore class를 보겠습니다.
LockFreeTaskQueueCore 내부에는 capacity, singleConsumer, mask, _next, _state, array가 존재합니다.
mask: 비트 연산에 사용됩니다. capacity - 1를 해주는 이유는 capacity가 16인 경우 비트로 나타내었을 때 10000 이고, -1을 해주게 되면 1111이 되게 됩니다. 이 mask를 통해 오버플로우를 막아주기 위해 사용됩니다.
_next: 다음 작업을 가리키는 포인터 역할입니다.
_state: 현재 큐의 상태를 나타냅니다. 큐가 FROZEN인지, CLOSED인지 상태를 나타냅니다.
array: 작업이 담기는 순환 큐 입니다.
위를 그림으로 나타내면 이러한 구조가 됩니다.
처음에 globalCPUQueue는 GlobalQueue 객체였고 GlobalQueue는 LockFreeTaskQueue를 상속받고 있고, 내부에 _cur이 있었습니다. 이 _cur은 atomic 내부에 LockFreeTaskQueueCore 객체를 가지고 있고 이 내부에 중요한 변수들이 _state, array, _next 였습니다.
array는 순환 큐 이기 때문에 오른쪽처럼 head, tail로 인덱스가 관리됩니다.
이제 LockFreeTaskQueueCore의 addLast 함수에 대해서 보겠습니다.
맨 위 if 문은 add를 실패하는 경우입니다.
withState를 보게되면
head와 tail을 가져오는 함수인 것을 알 수 있습니다.
위를 보면 비트 연산이 많은 것을 볼 수 있는데,
atomic(Long) 타입인 _state 하나로 head와 tail, 그리고 FROZEN, CLOSED 상태 등을 모두 관리하기 위해 이러한 비트 연산들이 많이 사용되게 됩니다.
그리고 여기서 보이는 shl, shr 같은 중위연산자들을 설명해보려 합니다. (shift right, shift left)
코틀린은 infix 키워드를 통해 중위연산자 기능을 제공합니다.
위처럼 infix 키워드로 add 확장함수를 만들게 되면
아래 ‘5 add 3’ 처럼 중위 연산자를 만들 수 있습니다.
다시 돌아와서, 이 부분은 큐가 가득 차는 경우에 FROZEN을 반환합니다.
그리고 이부분이 add를 성공하는 경우입니다.
tail+1을 하고 MAX_CAPACITY_MASK와 비트 연산을 통해 오버플로우 되지 않게 index를 늘려준뒤, compareAndSet을 통해 원자적 연산으로 tail을 업데이트해주는 것을 알 수 있습니다.
그리고 array에 작업을 넣어줍니다.
while문은 동기화에 대한 엣지 케이스의 경우를 고려한 부분입니다.
이 코드로 오기 전에 다른 곳에서 FROZEN 상태를 만들 수도 있기 때문에 FROZEN 상태인지 확인하고 FROZEN 상태면 next() 메서드를 실행합니다.
next() 함수는 먼저 markFrozen() 함수를 호출합니다.
이는 큐의 상태를 FROZEN으로 바꾸는 함수입니다.
그리고 나서, allocateOrGetNextCopy 함수가 실행되어 next가 null이 아니라면 next를 바로 가져오고, null이라면 allocateNextCopy 함수를 실행합니다.
allocateNextCopy 함수는 재할당하는 함수입니다. LockFreeTaskQueueCore의 capacity를 2배로 늘려 새로 생성한 next 객체를 볼 수 있습니다.
그리고 기존의 array가 가지고 있던 value를 모두 next.array에 복사해줍니다.
이때 kotlin에서 지원하는 null 연산자인 ?: 로 array요소가 null이면 Placeholder로 대채해서 넣어줍니다. FROZEN 상태가 되기 전에 다른 곳에서 요소에 든 Task를 소비했을 수도 있습니다. 그렇기 때문에 Placeholder로 대체해주는 과정을 합니다.
마지막으로 FROZEN상태를 해제해준 뒤에 next를 반환해주고 끝이 납니다.
next() 메서드가 끝나고 fillPlaceholder가 실행되는데 방금 null인 요소를 대신해 넣어준 Placeholder에 대해 새로운 task를 넣어주는 과정을 하는 메서드입니다.
이 과정이 끝나게되면 ADD_SUCCESS를 return하여 addLast 함수가 끝나게 됩니다.
그리고 위 (LockFreeTaskQueueCore)addLast를 호출한 LockFreeTaskQueue의 addLast도 끝나게 되고
맨 처음에 봤던 addToGlobalQueue도 끝나게 됩니다.
처음 이런 코드를 보게 되었는데 LockFree 자료구조에 대해서 알게 되었고, 동기화에 대한 엣지 케이스 부분을 모두 일일이 처리해주는 것이 신기했습니다.
그리고 스터디장님이 말씀해주시길, LockFreeTaskQueue와 LockFreeTaskQueueCore class 내부에서 singleConsumer인지 Boolean 형 값으로 관리하는 데, 자신이 보던 코드에서는 내부에서 관리해주는 것이 아니라 class 타입을 따로 나누어 사용한다고 하셨습니다. 코루틴에서 내부에서 관리해주게 되면 동기화 문제에 대한 것들이 내부에 함께 묶여있기 때문에 보기 어려워지는 것 같습니다.
'오픈 소스' 카테고리의 다른 글
오픈 소스 컨트리뷰터가 되다. (1) | 2023.12.23 |
---|