백엔드 개발자라면 비동기프로그래밍에 대해서 정말 많은 이야기를 들어보셨을거에요. 면접 단골 질문인 Node.js의 Event Loop부터 GoLang의 Goroutine까지 정말
비동기 라는 말을 안 들어봤으면 백엔드 개발자가 아니라는 말도 있을 정도니 정말 중요한 주제임을 틀림이 없는것 같습니다. 그럼 과연 비동기 프로그래밍은 무엇이고 왜 알아야하고 어떻게 사용하는게 좋을까요? 저는 이 시리즈에서 비동기 프로그래밍에 대해서 간략한소개를 해볼까 합니다. 비동기 프로그래밍을 소개한 다른 글은 많지만 이 시리즈는 개념부터 실제로 비동기 런타임을 코드로 구현을 해서 최대한 자세하기 설명 하는것을 목표로 합니다. 제가 지금 현업에서 러스트를 사용하고 있고 러스트를 좋아하기 때문에 이 글에서의 코딩 부분은 러스트로 할 예정입니다~
먼저 시리즈의 순서는
비동기 프로그래밍이란?
Scheduler 이해하기
Rust에서의 비동기 프로그래밍
Rust Tokio이해하기
입니다. 이 중 오늘은 비동기 프로그래밍이 무엇인지에 대해서 알아보겠습니다. 틀린 부분이나 설명이 모호한 부분이 있다면 댓글 혹은 이메일로 알려주시면 감사하겠습니다~
내일 저는 아침에 회사에 출근해서 다음과 같은 작업들을 해야합니다.
제가 갑자기 일기장에 써야 되는 일을 왜 여기에 적냐고요? 제가 위에 적은 일들을 어떻게 수행하는지가 바로 비동기 프로그래밍의 핵심이기 때문입니다. 저 자신을 CPU 라고 생각하고 제가 해야 하는 일 세 가지를 제가 실행해야 하는 세 가지 작업(task)이라고 해볼까요? 그러면 저는 코드 리팩터링을 먼저 다 마치고 아이스커피를 한잔 만들어서 마신 다음 아이유 노래를 들을 수 있습니다. 비효율적이라고요? 네 맞습니다. 저는 똑똑한 CPU(?) 이기 때문에 코드 리팩터링을 하면서 커피를 마시면서 아이유를 노래를 들을 수 있습니다. 왜냐하면 커피를 한 모금 마시고 코드 리팩터링 하다가 중간에 다시 커피를 마시고 일을 반복하면 되기 때문입니다. 아이유 노래는 항상 들으면 되고요! 즉, 하나씩 일을 처리하는것보다 세 가지 작업을 동시에 하면 제 시간을 효율적으로 사용할수 있고 빠른 퇴근이 가능합니다!
컴퓨터에 있는 CPU도 다르지 않습니다. CPU가 해야 하는 일들 중에 코드 리팩터링 처럼 중간중간 쉬는 타이밍이 있고 그 시간 동안 다른 일을 하면 더 효율적으로 작업들을 할수 있겠죠. 물론 일반적으로 프로그램의 코드는 순차적으로 진행됩니다. 한번에 한가지 사건만 발생하면서 말입니다.
fn drink(a: i32, b: i32) {
a * b
}
fn calculate(a: i32, b:i32, c: i32) {
multiply(a, b) + c
}
예를 들어 위와 같은 코드에서 calculate 함수는 multiply 함수가 끝나야지만 작업을 이어갈수 있습니다. 만약 multiply 함수가 오래 걸린다면 유저의 입장에서 보자면,전체 프로그램이 모두 멈춘 것처럼 보입니다. 만약 calculate함수 뒤로 할 일들이 많다면 multiply에서 오래 머물러 있는게 이상적인 상황은 아니겠죠? 그러면 어떤 게 하면 이런 상황을 해결할수 있을까요?
비동기라는 개념을 이해하기 위해서 우리는 먼저 멀티 스레딩에서 자주 등장하는 concurrency 와 parallelism 을 살펴보아야 합니다. 간단히 표현하자면 concurrency는 여러 작업을 동시에 스케쥴링 하는 방법이고 parallelism은 여러 작업을 동시에 처리하는 방법입니다. 뭐가 다르냐고요? 설명을 위해 제가 출근하고 해야하는 작업들로 돌아가보겠습니다. 그리고 concurreny와 parallelism 을 설명하기 위해 다음과 같은 가정을 추가해보겠습니다.
이 경우 저는 아이유 음악을 들으면서 코딩을 하거나 커피를 마시기가 불가능합니다. 귀와 손을 동시에 사용할수 없기 때문이죠. 그러나 코딩을 하면서 커피를 마실수는있습니다. 물론 완전한 동일한 시간에 코딩을 하면서 커피를 집어서 마실수는 없지만 커피를 계속 마시는게 아니라 커피 한 모금 입에 머금고 모듈 하나 리팩터링을 하고다시 커피 한 모금을 마실수는 있겠죠.
여기서 [커피를 한 모금 머금고 모듈 하나를 리팩터링 한다!] 이게 바로 여러 작업을 스케쥴링 해서 동시에 진행하는 방법입니다. 실제로 완전 동시에 작업들이 진행되지는않지만 유저 입장에서는 결국 두 가지 일을 한번에 하는 것과 다름이 없습니다. 그러나 이러한 꼼수(?)를 사용해도 저는 1번 가정에 막혀 아이유 노래를 듣지 못합니다.귀와 손을 동시에 사용 못하기 때문이죠. 그렇다면 어떤 방법이 있을까요? 네,커피 마시면서 코딩 하는 일을 제 팀원에게 부탁하면(!!!) 저는 편하게 아이유 노래를 즐길수 있습니다. 팀원(CPU)이 추가되면 여러 작업들을 실제로 동시에 진행할수 있게 됩니다. CPU를 추가해서 작업을 실제로 동시에 진행하게 만드는 것, 이게 바로parallelism입니다.
그렇다면 concurrency를 구현할때 어떻게 하나의 CPU가 여러 작업들을 스케쥴링 할까요? 누군가가 알려주지 않으면 CPU 스스로가 커피 한 모금 마실 타이밍을 계산할수있을까요?
CPU가 작업들을 스케쥴링 하는 방식은 크게 Preemtive Scheduling 과 Cooperative Scheduling로 나뉘게 됩니다. Preemtive Scheduling은 CPU가 모든 작업들에미리 일정 시간을 분배해서 일정 시간이 흐르면 현재 작업이 끝나지 않았더라도 작업을 멈추고 다른 작업을 진행하는 것입니다. 그렇게 반복을 해서 여러 작업들을 진행하게됩니다. 이에 비해 Cooperative Scheduling은 현재 실행되고 있는 작업에게 작업을 멈출지 말지에 대한 결정권이 있습니다. 만약 스스로 작업이 끝났거나 대기 시간이발생하면 CPU에게 알려주고 CPU는 다음 작업을 진행하게 됩니다. OS thread 에서 Cooperative Scheduling을 진행하기에는 위험한 부분이 너무 많아서, OS ThreadScheduling에서는 대부분 Preemtive Scheduling으로 Concurrency를 구현합니다. 이에 비해 뒤에서 다룰 green thread들을 스케쥴링 할때는 CooperativeScheduling을 주로 사용하게 됩니다.
비동기 프로그래밍 이야기하다가 갑자기 무슨 OS thread랑 Green thread 이야기가 나오냐고요? 맞습니다. 비동기 프로그래밍으로 가기 전 마지막으로 이해해야 하는 부분은 os thread vs Green thread의 개념입니다. 이 부분이 없어도 비동기 프로그래밍을 간단하게 이해할수는 있겠지만 조금 더 자세한 이해를 위해 여기서 간단하게 이 threading을 다루고 넘어가자 합니다.
먼저 thread는 프로세스가 할당받은 자원을 이용하는 실행의 단위입니다. 스레드는 프로세스 내에서 각각 Stack만 따로 할당받고 Code, Data, Heap 영역은 공유합니다. 즉, 각각의 스레드는 별도의 레지스터와 스택을 갖고 있지만, 힙 메모리는 서로 읽고 쓸수 있습니다. 한 스레드가 프로세스 자원을 변경하면, 같은 프로세스의 다른 스레드는 그 변경 결과를 즉시 볼 수 있습니다.
여기까지 설명을 드린건 OS thread 입니다. 즉, 학교 수업에서 들었던 스레드의 개념은 거의 모두 이 OS thread를 지칭합니다. 위에서 언급한대로 이 OS thread들의 스케쥴링은 OS thread scheduler가 담당하고 있고 어플리케이션을 작성할때 개발자가 이것들의 스케쥴링을 관리하는 경우는 없습니다(만약 OS개발자라면 얘기가 다르겠지만요).
이에 비해 Green thread는 user level thread로서 어플리케이션을 만들때 유저가 임의로 스레드를 만드는 형식입니다. 이 Green thread는 OS thread와는 달리 heap에서 할당을 받고 이 green threads들의 스케쥴링은 어플리케이션 레벨에서 스케쥴링 프레임워크를 사용합니다. golang의 goroutine이, rust의 tokio가 바로 그런 스케쥴러를 담당하고 있죠. 그렇다면 이 green thread 를 사용하는 이유는 무엇일까요? 먼저 OS thread가 하나밖에 없는 경우 user level 에서 concurrency를 구현하기 위해서 Green thread는 필수적입니다. 또한 Green thread를 사용함으로서 개발자가 스레드를 범용적으로 사용할수 있어 프로그램의 성능 향상의 효과도 기대할수 있겠죠. 어떤 성능 효과가 있는지는 다음 편인 <Scheduler 이해하기>에서 다루도록 하겠습니다.
여기서 그러면 OS thread랑 Green thread간의 매칭은 어떻게 이루어지나요? 라는 질문이 있으신 분들이 있으실거라고 생각합니다. 이 매칭또한 이야기할수 있는 부분이 많이 있겠지만 현대 컴퓨터들은 대부분 multi core CPU를 가지고 있기 때문에 Green Thread Scheduler들은 M개의 OS thread들을 N개의 Green Thread로 매칭 시키게 됩니다.이 매칭 과정이 어떻게 되는지는 각 프레임워크별로 다른기 때문에 가볍게 Green Thread Scheudler들은 M: N 모델을 사용한다는 점만 알고 계시면 좋을 것 같습니다. 시리즈의 마지막 편인 rust tokio이해하기에서 tokio는 이 매칭을 어떻게 하는지 한 번 살펴볼 계획입니다.
자 여기까지 오시냐고 고생이 많으셨습니다. 그러면 이제 바로 비동기 프로그래밍으로 들어가나요? 아닙니다. 마지막으로 async/sync, blocking/non-blocking에 대한개념들을 확실히 해야 합니다. 이 개념들이 헷갈리기 시작하면 뒤에 것들을 읽을때 카오스가 찾아오기 때문에 여기서 확실하게 짚고 넘어가도록 하겠습니다.
비동기 프로그래밍을 소개하는 글들을 읽다보면 blocking과 sync를 동일하게 취급할때가 종종 있습니다. 그러나 이 둘은 엄연히 다른 말입니다. 예를 들어 자바스크립트언어를 소개할때 a single threaded non-blocking asynchronous concurrent language 라는 표현을 많이 사용하고는 합니다. 만약 non-blocking과 async 가동일어였다면 애초에 non-blocking async라는 표현을 사용하지 않았겠죠. 그렇다면 async/sync와 blocking/non-blocking은 뭐가 다를까요?
쉬운 이해를 위해 예시를 한번 들어보겠습니다.
상황: 저는 팀원한테 코드 리뷰를 부탁한 상황입니다. 그리고 저는 지금 팀원에게 코드 리뷰가 다 되었는지 여쭤보려고 합니다.
blocking: 팀원한테 가서 코드 리뷰가 끝났냐고 물어보고 대답을 해줄때까지 그 자리에 기다립니다. 저는 당연히 다른 일을 못하겠죠
non-blocking: 팀원한테 가서 코드 리뷰가 끝냤나고 물어보고 대답을 기다리지 않고 다시 제 자리로 돌아와서 할일을 합니다.
sync: 팀원한테 가서 코드 리뷰가 끝났는지 물어보고 대답을 해줄때까지 기다립니다. 대답을 해줄때까지 기다리되 대답에 따라 제가 하는 일이 달라집니다.
async: 팀원한테 가서 코드 리뷰가 끝냤나고 물어보고 대답을 기다리지 않고 다시 제 자리로 돌아와서 할일을 합니다. 그러면 팀원이 코드 리뷰가 끝나자마자 제 자리로와서 저한테 끝났다고 알려주기로 합니다.
차이가 보이시나요? 이 예시로 완벽하게 설명하기는 힘들겠지만 즉, blocking/non-blocking은 제어권을 누가 가지고 있냐에 대한 질문이고 sync/async 는 작업 완료부에 대한 질문입니다. 일반적인 상황에서 제어권을 기다린다는 의미는 작업 완료 여부에 대한 값을 신경쓰기 때문에 blocking & sync와 non-blocking & async는대부분 같이 적용이 되기도 합니다. 그러나 non-blocking & sync의 경우도 발생을 하는데 예를 들어 제가 팀원한테 코드 리뷰가 끝났냐고 물어보고 아니라는 대답을들으면 1분 마다 찾아가서 또 물어보는 상황이라면 제어권을 계속 제가 가지고 있으므로 non blocking 이기는 하지만 작업 완료 여부에 따라 제 행동이 달라지기 때문에sync 이기도 합니다.
자 그렇다면 비동기 프로그래밍은 asnyc + non-blocking의 영역입니다. 즉, 위 예시를 통해 정리하자면 비동기 프로그래밍이란에서 저의 행동패턴을 정리하자면 => 저는팀원한테 코드 리뷰가 끝났는지 물어보고 대답을 기다리지 않고 자리로 돌아와서 제가 할 다른 일들을 하고있다가 팀원이 코드 리뷰가 끝나면 저한테 와서 끝났다고 말을해주게 되고 이 결과에 따라 제가 다음에 할일이 정해지게 됩니다.
그럼 과연 여기서 저는 누구일까요? 네 맞습니다. 저는 바로 Green Thread Scheduler입니다. 스케쥴러는 여러 작업들을 non blocking 하게 실행하고 작업들의 결과값을받으면 다시 어떤 행동을 취할지는 결정해주는 비동기 프로그래밍의 코어입니다. 그러면 다음편에서는 Scheduler에 대해 알아보겠습니다~
이번 편을 통해 비동기 프로그래밍의 기본 개념들에 대해서 공부를 해보았습니다. 제가 부족한 부분이 많아 설명이 잘못되었거나 모호한 부분이 있을수 있습니다. 댓글로 편하게 지적해주시면 바로 수정하겠습니다~ 감사합니다~
이 글은 제가 근무하고 있는 크래프트테크놀로지스의 지원을 받아 작성되었습니다. 저희 회사에서는 여러 제품들에서 rust를 사용하고 있으며 Rust를 사용해서 더 멋있는 제품을 만드려고 노력중입니다. 같이 빠르고 안전한 금융 서비스분들을 아래 링크를 참조해주세요!
https://medium.com/@itIsMadhavan/concurrency-vs-parallelism-a-brief-review-b337c8dac350
https://velog.io/@wonhee010/%EB%8F%99%EA%B8%B0vs%EB%B9%84%EB%8F%99%EA%B8%B0-feat.-blocking-vs-non-blocking
http://www.programmr.com/blogs/difference-between-asynchronous-and-non-blocking
좋은 글 감사합니다!
중요한 건 아니지만, fn calculate(a: i32, b:32, c: i32) 에서 i32가 아니라 32로 오타가 있네요!^^