[번역: Async Basics with Rust] 동시성 VS 병렬성(Concurrent vs Parallel)
이 포스팅은 Async Basics with Rust의 글을 번역한 것입니다. 오역, 의역, 생략이 있으니 감안하여 읽어주십시오.
동시성과 병렬성의 차이는 무엇인가?
이 주제에 대해 곧바로 동시성이 무엇인지 정의함으로써 파헤쳐보자. 병렬 상의 동시성과 쉽게 헷갈릴 수 있어서 시작부터 두 가지를 명확히 구분해둘 것이다.
동시 실행(Concurrency)이란 동시에 많은 것을 **처리하는** 걸 말한다.
병행(Parallelism)이란 동시에 많은 일을 **수행하는** 걸 말한다.
우리는 멀티태스킹
을 동시에 여러 개의 작업을 진행한다는 개념으로 쓴다. 이러한 다중 작업에는 두 가지 방법이 있다.
하나는 작업을 동시에(;함께; 겸임) 진행하지만, 실제 같은 시간에 하지는 않는 것이며,
또 다른 방법은 병렬적으로 실제 같은 시간에 여러 작업을 진행하는 것이다.
몇 가지를 정의해보자.
리소스(Resource)
작업을 진행하는 데에 필요한 것. 리소스는 제한되어있다.
한 예로 CPU의 시간이나 메모리를 들 수 있다.
작업(Task)
진행하면서 어떤 종류의 리소스를 필요로하는 기능 집합(A set of operations)이다.
하나의 작업은 몇 개의 sub-operations로 구성된다.
병렬성(Parallel)
정확히 같은 시간에 독립적으로 일어나는 일.
동시성(Concurrent)
동시에
진행중(in progress)
인 작업들을 말하지만, 반드시 같은 시간에 진행되는 것은 아니다.이는 중요한 차이점이다. 만약 두 작업이 동시에 실행됐지만 병렬적이지는 않을 때, 그 작업들은 stop(멈춤)하거나 resume(재시작)할 수 있어야 한다.
주석 > 왜냐하면 병렬적이지 않은 것은 위의 [그림2 - Parallel]과 같이 한 작업이 계속해서 CPU를 붙잡고 항상
진행중
상태에 있지 않기 때문이다. 멈추고 재시작하기를 반복한다.
따라서 동시 실행 속성을 가지고 있다면 interruptable(끼어들 수 있는)하다고 말한다.
내가 사용하는 심상모형(mental model)
나는 우리가 병렬성이나 동시성을 가진 프로그램을 만들 떄 어려워하는 이유가 일상에서 일어나는 사건들을 모델링하는 방법에서 기인한다고 생각한다. 우리는 대게 잘못된 직관으로 대략적인 정의를 내리는 경향이 있기 때문이다.
concurrent의 사전적 정의는 parallel과의 차이를 인지하는 데에 도움을 주지 않는다.
나로 말할 것 같으면, 병렬성과 동시성의 차이점이 왜 필요한지를 떠올린 게 시작이었다.
이들이 필요한 이유는 리소스의 활용도와 효율성과 관련된 모든 것에 있다.
효율성이란 어떤 일을 하면서, 또는 바라는 결과를 내는 데에 쓰이는 자원, 에너지, 노력, 돈, 그리고 시간을 낭비하지 않는 (대게 측정할 수 있는) 능력을 말한다.
병행(Parallelism)
작업을 수행하면서 리소스를 계속해서 늘린다. 이는 효율성을 고려하지 않는다.
동시 실행(Concurrency)
효율성과 리소스 활용도 모두를 고려한다. 동시 진행은 절대 단 하나의 작업을 더 빠르게 만들 수는 없다. 대신 리소스를 더 효율적으로 운용하고, 그럼으로써 작업들의 집합(a set of tasks)이 더 빠르게 끝나도록 한다.
경제학에서 몇 가지 유사점을 찾아보자.
상품 제조 사업에서는 린(LEAN)이 대표적이다.
린 이라는 기술을 사용함에 있어 가장 이점은 기다리는 시간과 가치없는 작업을 제거하는 것이다.
프로그래밍에서 말할 것 같으면, blocking과 polling을 피한다고 말할 수 있다.
동시성 및 I/O와의 관계
지금까지 말한 걸 보면, 비동기 코드를 작성하는 데 리소스를 최적으로 사용할 때야 비로소 의미가 있다.
프로그램을 짤 때 동시성이 도움이 되지 않는 경우도 있다. 병렬로 작업할 파트들로 나눌 수 있다면 더 많은 리소스를 할당하는 식으로 문제를 해결할 수 있다.
동시성에 관한 두 가지 주요 이용 사례가 있다:
- 입출력이 수행되는 중에 일부 외부 이벤트가 발생할 때까지 기다리는 상황.
- 여러 가지에 집중해야할 때 한 가지에만 너무 오래 기다리는 걸 방지해야하는 상황.
첫 번째는 기본적인 입출력 예제이다: 당신이 한 가지 작업을 진행하기 전에 네트워크 호출이나 DB 쿼리 등이 발생할 때까지 기다려야하는 상황이다. 그러나 지금 다른 할 일도 많기 때문에 다른 작업을 계속하다가 작업(네트워크 호출 등)이 준비가 됐는지 정기적으로 확인하거나 준비가 됐을 떄 알림을 받아야 한다.
두 번째는 UI 단에서 자주 일어나는 일이다. 당신이 한 개의 코어만 가지고 있다고 하자. 그럼 CPU에서 집중적으로 작업을 수행하고 있는데 대체 어떤 UI가 무반응을 피할 수 있을까?
음, 당신이 지금 하고 있는 작업이 뭐든간에 멈추고, “UI 갱신”을 하고, 그 후에 하려고 했던 일을 재시작할 수도 있다. 이렇게하면 작업을 1초에 60번 중지/재시작 해야한다. 그러면 당신은 결국 대략 60Hz의 새로고침 빈도를 반응하는 UI를 가지게 될 것이다.
OS에서 제공하는 스레드에 관하여
I/O 처리 전략에 관해 이야기하면서 스레드에 대해 좀 더 다룰 거지만 여기서도 언급하겠다. OS 스레드를 사용할 때 한 가지 문제는 코어에 매핑되는 것처럼 보인다는 것이다. 대부분의 운영 체제가 스레드 수가 코어 수와 같을 때까지 하나의 스레드를 하나의 코어에 매핑하려고 시도하더라도 이게 반드시 올바른 심상 모델은 아니다.
코어보다 많은 스레드를 생성하면, OS는 스레드간에 스위치를 수행하고 각 스레드에 실행시간을 제공하는 스케줄러를 사용하여 각 스레드를 동시에 진행하도록 할 것이다. 그리고 시스템에서 프로그램은 겨우 하나만 실행되지 않는다는 걸 명심해야한다. 다른 프로그램도 여러 개의 스레드를 생성할 수 있고, 이는 CPU에 있는 코어보다 더 많은 스레드가 있음을 의미한다.
그러므로, 스레드는 병렬적으로 작업을 수행하도록 하는 수단이 된다. 이는 동시성을 달성하는 수단이기도 하다.
이건 동시성에 관한 마지막 파트로 이어진다. 이제 일종의 참조 프레임을 정의해야 한다.
참조 프레임 바꾸기
당신의 관점에서 봤을 때 완벽하게 동기적인 코드를 짰다고 하자. 잠시 멈춰서 운영 체제 관점에서 이게 어떻게 보일지 생각해봐라.
운영 체제는 당신의 코드를 처음부터 끝까지 실행하지 않을 수 있다. 매순간 프로세스를 멈추고 다시 시작하길 반복할 것이다. CPU는 당신이 보기에 이 작업에만 집중하고 있다고 생각하는 동안에도 멈추고 일부 입력을 처리하고 있을지도 모른다.
그러니 동기적 실행은 그저 환상이다. 하지만 프로그래머로서 당신의 관점에서, 그렇지만도 않다. 이게 요점이다:
다른 맥락 없이 동시성을 말할 때, 당신은 프로그래머이며 당신의 코드는 참조 프레임이 된다. 만약 이를 염두에 두지 않고 동시성에 대해 이해하려한다면 혼란스러울 수 있다.
즉, 참조 프레임을 염두에 둬야 한다.
아직 복잡하게 들릴 수 있다. 이후 비동기 코드와 함께 작업하면서 이를 계속 상기해낸다면 복잡함은 점점 덜게 될 것이라 약속한다.
[번역: Async Basics with Rust] 동시성 VS 병렬성(Concurrent vs Parallel)