초보개발자의 병렬처리와 동시성 그리고 AKKA 맛보기

Jaeyeoul Ahn
월요일 오후 9시
12 min readMay 2, 2022

--

Parallelism(병렬)과 Concurrency(동시,병행)를 들어본적이 있으신가요?

모르셔도 괜찮아요. 지금부터 알아갈테니까요.

예를 들어볼게요. 콜라자판기가 있고, 목이 마른 사람들이 있다고 합시다. 이때 콜라를 사려는 사람들은 줄을 설텐데요.

‘자판기가 한개 인 경우와 두개인 경우, 모든 사람이 콜라를 빨리 먹을 수 있는 방법은 무엇일까요?’

자판기 2개 라고 생각 하셨을것 같아요. 하지만 상황에 따라 다를거에요. 아래 상황은 약간 무리가 있지만.. 상상을 해보죠.

  • 자판기가 한대만 놓여있는 곳의 자판기는 결제가 빠르게 이뤄지는 자판기
  • 자판기가 두대 놓여있는 곳의 자판기는 결제가 느린 자판기

어때요? 다르죠? 본론으로 돌아오죠. ‘모든사람들이 콜라를 마실수 있게 되는 총 시간은 같다.’ 라고 제약사항을 두고 작업을 처리하는 방법을 살펴봤을때 Parallelism(병렬)과 Concurrency(동시,병행)는 아래와 같아요.

https://luminousmen.com/post/concurrency-and-parallelism-are-different

작업을 기준으로 처리되는 방법을 시간순으로 나열하면 아래와 같을거에요.

https://learningswift.brightdigit.com/asynchronous-multi-threaded-parallel-world-of-swift

동시성은 독립적인 여러 일을 한꺼번에 구성하고 다루는 데 관한 것이다.

병렬성은 여러 일을 한꺼번에 실행하는 데 관한 것이다. — Rob Pike https://rakhim.org/summary-of-concurrency-is-not-parallellism-a-talk-by-rob-pike/

멀티프로세싱, 멀티스레딩

병렬처리는 컴퓨터에서 프로세스와 스레드를 활용하여 구현합니다. 자세한 내용은 운영체제를 학습하시면 조금더 도움이 되실꺼지만, 잠깐 짚고 넘어가도록 해보죠.

프로세스는 컴퓨터에서 실행되는 프로그램을 말해요. 프로그램을 실행하는 것은 무언가의 목적을 위해 작업을 수행하는 일이죠. 이 작업들은 운영체제를 통해서 CPU, Memory등을 이용해서 수행을 해요. 운영체제는 커널을 통해서 아래와 같은 프로세스의 상태를 규칙으로 정의하고 관리하죠.

https://ko.wikipedia.org/wiki/프로세스

요즘에는 CPU가 멀티코어를 지원해서 컴퓨터가 프로세스를 하나씩 수행하지 않고, 여러개의 프로세서를 동시에 사용하고 있어요. 예를들면 우리는 지금 인터넷 브라우저(크롬, 사파리 등)을 통해서 지금 글을 읽으면서 동시에 카톡을 켜서 메시지를 읽고, 음악도 들을 수 있죠. 리눅스 환경에서 보면 아래와 같이 보일거에요.

https://www.makeuseof.com/htop-linux-process-manage

이런 것을 멀티프로세싱이라고 해요. 멀티프로세싱을 위한 프로세서의 구조는 SMP, MPP 등으로 메모리 공유방식에 따라서 분류 할 수 있어요. Flynn 분류법에 따라서 SISD, SIMD 등 명령어와 데이터 입력의 개수에 따라서 구분하기도 하구요.

https://ko.wikipedia.org/wiki/대칭형_다중_처리
https://slideplayer.com/slide/15489810

이런 용어를 알아서 어떤 도움이 되냐구요? 저는 아래와 같은 아티클을 읽을 때 배경 지식으로 활용 할 수 있어서 도움이 되고, 전체 시스템 아키텍처 설계시에도 힌트를 얻을 수 있어서 도움이 되더라구요.

https://dzone.com/articles/hadoop-vs-database-vs-cloud-dwh

그럼 멀티스레딩은 무엇인가요?

멀티프로세싱을 통해서 프로세스를 여러개 병렬로 실행을 하도록 하면 Context Switching과 메모리 공유비용을 고민해야해요. SMP의 경우에는 시스템 버스의 처리량에 따라서 성능저하가 발생 할 수도 있고, MMP의 경우에는 프로세스간 메시지를 주고 받을 경우, 메시지 패싱 기법의 효율을 고민해야해요. CPU의 코어갯수도 한계가 있어서 병렬로 한번에 작업을 할 수 있는 프로세스가 제한적이죠.(그래서 GPU의 도움을 받아서 연산의 제약을 확장하기도 해요. 컴퓨팅능력의 외연확장이라고 할 수 있을까요?ㅎ)

코드와 데이터는 ‘공유’해야하는데, 여러 작업을 병렬로 처리해야 하는 상황이라면 도움이 되는 방법이 스레드를 사용하여 멀티스레딩을 이용하는 것이 좋은 선택이 될 수 있을 것이에요.

스레드는 일의 처리 흐름 단위이며 스레드가 여러 개이므로 ‘멀티스레드’ 또는 다중스레드라고 부르며, 이러한 작업으로 수행하는 동작이 ‘멀티스레딩’이다. 1950년대부터 이러한 개념이 정립되었으며, 수 십년이 지난 지금도 ‘작업의 최소 처리 단위’를 스레드 단위로 취급하고 있다. 참고로 컴퓨터에서의 작업은 여러 명령어(Instruction)들로 구성되며, 명령어의 실행은 사이클(클럭) 단위로 처리한다. — https://namu.wiki/w/SMT

다만, 여기서 공유하는 데이터를 병렬로 처리하다 보면 경쟁상태(Race Condition) 등의 문제가 생길수 있어요.

예를 들어 볼까요?

아래 코드를 수행하면 콘솔창에 숫자 몇이 나올까요? 20000?

아마도 그때그때 다르게 나올거에요. 왜냐면, int count의 값을 두개의 스레드가 업데이트 하고 있기 때문이죠.

자바는 기본적으로 객체에 lock을 지원해서 스레드의 race condition을 해결하는 것을 지원해요. 이를 Builti-in Lock, Intrinsic Lock 혹은 Monitor lock이라고 해요. 이용하는 방법으로 synchronized 가 있죠. 이외에는 java.util.concurrent.locks 패키지를 통한 Explicit Lock 등을 이용 할 수도 있어요. 이번 글에서는 int count 를 우리는 Critical Section 으로 보고 이를 해결 할 수 있는 방법으로 자바의 synchronized 를 사용 하도록 해요.

어때요 참 쉽죠?

그런데 만약, Counter의 count 가 public으로 캡슐화가 덜 던 상태라면 어떤가요?

그 상태에서 다른 개발자가 Counter의 count가 스레드로 운영되는 상태인지 모른다면, 어떤일이 벌어질까요?

뭐.. 또 Race Condition이 생겨버렸네요. ㅜㅜ

해결하려면 java.util.concurrent.atomic.AtomicInteger 를 사용하는 방법도 있어요.

멀티쓰레드 코딩을 통해서 작성을 하는 것은 어렵지만 익숙해진다면 쉬울수도 있어요. 그렇지만 함께 개발하고 운영해나가는 일은 더욱 복잡할 것이에요. 위와 같이 엄격하게 쓰임새를 모른다면 Thread-Safe 한 환경이길 기도하듯 코드를 작성하거나, 모든 상황에서 쓰레드를 사용한다고 가정하고 작성을 하는 행위가 이뤄질 수도 있어요.

디버깅의 경우에도 쓰레드 환경을 고려하지 않는다면 아래와 같이 시퀀스 다이어그램도 SRP, 캡슐화에 따라서 행위가 보장된다고 생각하고 일관된 방향으로 담백하게 표현할 수 있어요.

https://doc.akka.io/docs/akka/current/typed/guide/actors-motivation.html

그렇지만 스레드가 개입하는 순간 그림이 복잡해지죠.

https://doc.akka.io/docs/akka/current/typed/guide/actors-motivation.html

액터모델

많은 개발자들은 비즈니스를 구현하는 일을 하고 있어요. 시간이 많지 않고 비즈니스 로직에 집중하기 바빠요.

그래서 병렬, 동시성 구현과 비즈니스 로직 구현간 관심사를 분리하고 지원 할 수 있는 것이 필요했어요. 이런 상황에 도움이 되는 것이 있으니 바로 액터모델이에요.

액터는 ‘쓰레드’ 혹은 ‘객체’와 구별되는 추상이다. 액터가 차지하는 메모리 공간은 어느 다른 쓰레드 혹은 액터가 접근할 수 없다. 다시 말해서 액터 내부에서 일어나는 일은 어느 누구와도 ‘공유’되지 않는다. https://zdnet.co.kr/view/?no=20140213110522

객체지향에서는 개발자는 캡슐화된 객체의 책임을 이해하고 객체간 메시지를 주고 받도록 구현을 해요. 액터는 객체지향과 비슷해요. 액터도 상태를 캡슐화하고 메시지를 전달하는 방식으로 다른 액터와 의사소통을 하죠. 차이가 있다면 액터는 다른 액터들과 동시에 동작을 수행한다는 점이에요. 객체지향과 다른점은 클래스간 메서드를 호출하는 형식에 메시지 전달하는 방식이 아닌, 진짜로 메시지를 주고 받아요

https://www.brianstorti.com/the-actor-model

액터는 send, create, receive 세가지의 역할을 수행해요. 각 액터는 상태를 공유하지는 않고, async 통신을 이용해서 메시지를 주고 받아요. 각 액터는 메시지를 mailbox라고 부르는 Queue에 넣고 순차처리를 하죠.

Actor의 구현

이런 Actor모델은 Elrang이나 Elixir를 통해서 구현 할 수 있고, Akka와 같은 프레임워크를 이용하여 사용 해 볼 수 있어요.

아래와 같이 Akka에서는 Actor로 만들 수 있어요.

예시로 Greeter, GreeterBot, GreeterMain 세개의 액터를 만들어 실험을 해보도록 해요.

각 액터는 아래와 같은 행위를 목적으로 해요.

  • Greeter — Greeter
  • GreeterBot — Greeted
  • GreeterMain — Create Actor : Greeter and GreeterBot

Greeter라는 Actor와 Greet라는 행위를 정의를 해주고, 정의된 Actor를 create 할 수 있는 Actor를 정의해줘요. 그리고 시작을 해요.

자세한 코드는 아래 주소에서 확인해보세요.

Greeter 액터는 1개, GreeterBot 액터는 1개, GreeterMain 액터 1개로 구현하였을때 아래와 같이 쓰레드가 추가되요.

GreeterBot의 메시지 전달을 1개로 줄이면 아래와 같이 thread도 적게 사용해요.

또한 일정시간 경과후에는 Thread가 줄어들어 default와 internal 두개만 남아요.

만약 greeter를 n 개 들린다면 아래와 같을 것이에요.

지금까지는 Actor 모델에 관해서 대략적으로 사용을 했지만 동시성문제를 다뤄보지는 못했어요. 다만 akka framework가 스레드를 운영하는 IoC 구조라는 것은 확인했어요.

그렇지만. 아직까지는 Observer Pattern을 구현한 듯한 느낌이에요.

다음 글에서 akka를 이용한 동시성 문제를 다뤄보도록 약속하며 글을 마무리하려고 해요.

--

--