-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create [Concurrency] Actor Model과 CSP.md
- Loading branch information
Showing
1 changed file
with
212 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
# Acotr Model과 CSP | ||
공부하게 된 계기 <br> | ||
|
||
동시성 문제를 해결할 수 있는 방법은 여러가지이다. 사람들은 주로 Locking에 대해서만 생각하지만, 결국 Locking은 "가변 공유 자원"을 여러 작업 주체가 동시에 접근하면서 생기는 문제를 해결하기 위해 한 주체만 접근 가능하게 한다. <br> | ||
결국 문제인 것이 "가변 공유 자원에 대한 동시 접근"이라면 동시 접근만을 막는 것이 해결 방법일까? <br> | ||
|
||
차라리 자원을 공유하지 않으면 어떨까? 공유메모리를 사용하지 않는 방식으로도 동시성 문제를 해결하는 두 모델을 알아보겠다. <br> | ||
|
||
간단하게 공부한 내용을 정리한 것이고, 나중에 고루틴과 엮어 글을 써보면 좋을 것 같다. 내용 추가가 필요한 부분에 TODO로 표현했다. | ||
|
||
# 1. Actor Model | ||
|
||
함수형 프로그래밍은 가변 상태를 사용하지 않음으로써 공유된 가변 상태가 야기하는 문제를 원천적으로 피한다. <br> | ||
|
||
**이에 비해 Actor Programming 가변 상태를 사용하긴 하지만 공유되는 부분을 제거한다.** <br> | ||
생각해보면 그렇다. 결국 동시성 문제는 가변 데이터에 여러 작업 주체들이 접근하는 것이 문제인건데, 공유하지 않으면 그만 아닌가? <br> | ||
**Actor는 자신이 가진 상태를 캡슐화하고, 다른 객체들이 직접 조작하는 것을 막는다. 대신, 메시지를 전달하는 방식으로 다른 액터와 의사소통한다.** 나는 일종의 메모리 수문장들이라고 생각했다. 내가 맡은 메모리는 오직 나를 거쳐서만 접근할 수 있는 것이다. <br> | ||
|
||
|
||
Actor는 객체지향 프로그램에서 사용하는 "객체"와 비슷하다. 액터는 자신의 상태를 가지고 있고, 다른 액터들과 소통한다. <br> | ||
차이는 액터들은 다른 액터들과 동시에 동작을 수행한다는 면에서 객체지향과 차이가 있다. <br> | ||
그리고 단지 메서드를 호출하는 것에 불과한 일반적인 객체지향 스타일의 메시지 전달 방식과는 달리, **액터는 진짜로 메시지를 주고 받는 방식으로 의사소통을 한다.** | ||
|
||
Actor 모델은 거의 모든 언어에서 동시성 문제를 해결하기 위해 사용될 수 있는 범용적인 방식이지만, 실제로는 Erlang과 깊은 관계를 맺고 있다. | ||
|
||
# 1.1 Message와 Mail Box | ||
Actor 모델은 Elang과 관련이 깊다. Elang과 얼랭 가상 기계에서 동작하는 엘릭서의 세상에서는 액터가 "프로세스"로 불리운다. 이 프로세스는 우리가 아는 프로세스 혹은 스레드 보다도 가볍다. 어떻게 그럴 수 있을까? | ||
|
||
## 1.1.2 Mail Box - Queue | ||
액터 프로그래밍의 특징 중에서 가장 중요한 것은 **액터들이 주고 받는 메시지들은 비동기적인 방식으로 전달된다는 점이다.** <br> | ||
자신만의 공간을 가진 액터들은 메시지를 교환할 때, 직접 주고 받지 않는다. 단시 Mail Box에 메시지를 전달한다! <br> | ||
|
||
이게 무슨 의미일까? **모든 Actor들은 메시지를 보내는 것도, 받는 것도 상대 Actor의 상황에 구애받지 않고 자신의 상황에 맞게 메시지를 보내고 받는다.** <br> | ||
|
||
이는 Actor들이 분리되어 있음을 의미한다! Actor는 다른 액터들과 동시에 동작하고 있지만, 자신이 받는 메시지는 순서대로 처리한다. 따라서 우리는 메시지를 "보낼 때만" 동시성에 대해 고민하면 된다. <br> | ||
|
||
## 1.2 메시지 전달받기 | ||
Actor는 보통 살아있는 내내 Receive로 메시지를 받아 처리하는 작업을 무한히 반복한다. Erlang의 Actor들은 무한 루프를 돌며 자신을 호출하고, 메시지가 오기를 기다린다. 메시지가 오면 작업을 수행할 뿐이다. | ||
|
||
## 1.3 프로세스 연결하기 | ||
Actor 모델에서 안전하게 프로그램을 종료시키려면 어떻게 해야 할까? | ||
|
||
1. Mail Box가 비게 되는 경우, Acotr에게 이제 멈추라고 알려준다. : 명시적인 종료 메시지를 받을 수 있도록 구현하면 해결할 수 있다. | ||
2. Actor가 실제로 종료했는지 확인할 수 있어야 한다. | ||
|
||
## 1.4 상태 액터 | ||
|
||
상태 Actor를 만들기 위해서 가변 변수를 도입해야 할 것 같지만 재귀만 있으면 된다. | ||
|
||
```py | ||
defmoudle Counter do | ||
def loof(count) do | ||
receive do | ||
{:next} -> | ||
IO.puts("Current count : #{count}") | ||
loop(count + 1) | ||
end | ||
end | ||
end | ||
``` | ||
|
||
위 함수를 보면 따로 상태를 만들지 않았다. 호출할 때마다 인수를 늘려줬을 뿐이다. 가변 변수를 사용하지 않았지만, 상태를 표현할 수 있었고, 이 액터는 메시지가 하나씩 처리되기 때문에 동시성 버그를 염려하지 않아도 되서 좋다. | ||
|
||
|
||
## 1.5 쌍방향 커뮤니케이션 | ||
|
||
**액터 사이에선 메시지가 비동기적으로 전달된다.** 즉, 메시지를 보내는 액터는 블로킹되지 않은다. 단지 Mail Box에 요청을 넣을 뿐이다. <br> <br> | ||
|
||
그런데, 아까 부터 이런 이야기를 계속 하는데 그건 **Queue에 메시지를 넣기만 하는 방식으로 소통하는건 응답이 없을 때나 가능한거 아냐? 만약에 응답이 필요하면 어떻게 할 것인가??** <br> | ||
|
||
Actor 모델은 이런 응답 과정을 직접 지원하지 않는다. **하지만 메시지에 보내는 프로세스의 식별자를 저장하거나 이름을 붙이는 등의 여러가지 식별을 위한 방법을 활용해, 메시지를 전달받은 액터가 응답을 보내는 동작을 구현할 수 있게 한다.** <br> | ||
|
||
|
||
# 1.6 에러 처리와 유연성 | ||
Actor는 어떻게 "장애 허용 수준"이 높은 코드를 작성할 수 있도록 도와줄까? | ||
|
||
## 1.6.1 장애 감지 방식 | ||
### 1.6.1.1 양방향 연결 | ||
**액터 모델에서 장애 감지를 위해 액터끼리 양방향 연결을 적용할 수 있다.** 액터끼리는 메시지를 주고 받을 때, 직접 주고 받는 것이 아닌 Mail Box를 통해 통신을 주고 받는다. 이때 만약 액터끼리 연결되어 있지 않다면, Mail Box가 비어있는 것 만으로는 다른 액터의 장애 여부를 추측할 수 없기 떄문이다. <Br> | ||
양방향 연결 이후, 액터들은 서로 비정상 종료가 발생할 때, 그 사실을 알리면 된다. | ||
|
||
### 1.6.1.2 감시자 설정 | ||
혹은 장애 감지만을 위한 하나 이상의 Worker를 할당하는 방식이 있다. **즉, 감시자 역할을 할 하나의 프로세스를 두는 것이다.** 이렇게 하면 수많은 액터들이 서로 연결될 필요 없이 종료 여부를 주고 받을 수 있을 것이다. | ||
|
||
|
||
<!-- ## 2.2 메시지의 전달은 어디까지 보장되는가? --> | ||
<!-- Actor 모델에서 메시지 전달은 어디까지 보장되는가?는 중요하다. <br> --> | ||
<!-- 여기서는 엘릭서 언어의 보장 범위를 예시로 보이는데 --> | ||
<!-- 1. 어떠한 에러도 없다면 메시지 전달이 보장된다. --> | ||
<!-- 2. 어떤 에러가 발생하면, 우리는 에러가 발생했다는 사실을 알게 된다. --> | ||
<!-- 이 두번째 보장 덕분에 엘릭서는 높은 장애 허용 수준을 보장하는 코드를 작성할 수 있다. --> | ||
|
||
## 1.7 에러-커널 패턴 (이해가 잘 안가서 Pass) | ||
<!-- Quick Sort를 고안한 토니 호어는 유명한 말을 남겼다. <br> --> | ||
|
||
<!-- > 소프트웨어를 설계하는 데엔 두 방법이 있다. <br> 하나는 단순하게 만들어 아무런 결함이 없도록 만드는 것이고, <br> 다른 하나는 복잡하게 만들어서 눈에 드러나는 결함이 없도록 만드는 것이다. --> | ||
|
||
<!-- Actor 프로그래밍은 이러한 통찰에 따라 장애 허용 수준이 높은 코드를 작성하는 것을 지원해준다. 그 방법이 에러-커널 패턴이다. <br> <br> --> | ||
<!-- 어떤 소프트웨어 시스템에서의 에 --> | ||
|
||
|
||
## 1.8 크래시 하게 놔둬라! | ||
이해가 안 가서 일단은 Pass. <br> | ||
간단히 설명하자면, 크래시 하게 놔둬라!는 Actor 모델의 핵심 컨셉 중 하나인데, 프로세스의 크래시를 그냥 허용한다. 덕분에 어느 정도 장애 허용 코드를 작성할 수 있다. | ||
|
||
|
||
# 1.9 Actor 분산 | ||
엑터 모델이 다른 모델에 비해서 뛰어난 부분은 분산에 대한 지원에 있다. 무려 다른 시스템에서 동작하는 Actor에 메시지를 보내는 것은 동일한 컴퓨터에서 동작하는 액터에 메시지를 보내는 것 만큼이나 쉽다! | ||
|
||
<!-- ## 1.9.1 OTP | ||
OTP는 액터와 감시자를 세팅해주는 라이브러리이다. Open Telecom Platform의 줄임말이다. (Erlang 자체가 원래 텔레콤 영역에서 시작됨 TODO : 더 찾아보기) <br> | ||
# 객체지향과 액터 모델 | ||
객체 지향 프로그래밍의 아버지인 앨런 케이는 객체지향의 본질이 객체가 아닌 "메시지"라고 말했다. <br> | ||
위대하고 성장 가능한 시스템을 만들 때의 핵심은 객체 내부의 속성이나 행위가 아니라 각각의 모듈이 의사소통 하는 방식이다. | ||
## 장점 --> | ||
|
||
|
||
|
||
# 2. CSP | ||
|
||
우리가 먼 길을 떠난다고 생각해보자. 어떤 자동차를 타고 어떻게 갈 것인지에서 의논할 것이다. <br> | ||
|
||
이때 자동차광은 모든 신경을 오직 자동차에만 쏟기 쉽상이다. 터보엔진과 자연흡기엔진 사이의 차이점. 혹은 엔진을 차체의 중앙에 놓는 방법과 앞에 놓는 방법 등 오로지 자동차에 대해 고민하는 동안. "길"에 대한 것은 잊어버리게 된다. <br> | ||
우리가 원하던 곳에 어떻게 갈 것이고 얼마나 빠르게 갈 것인가는 자동차 자체가 아니라, 길들이 어떻게 연결 되어있는가에 의해 결정된다. <br> | ||
|
||
|
||
마찬가지로 메시지 전달 시스템에서 기능과 용량은 메시지를 주고 받는 개체들이나 그 안에 담긴 내용이 아닌, 메시지가 전달되는 경로 그 자체에 의해 결정된다. <br> | ||
|
||
|
||
Actor 모델은 독립적인 메모리, 동시에 실행, 메시지를 주고 받으며 소통하는 액터들로 구성되었다. 액터는 자신과 단단히 연결된 Mail Box에 메시지를 읽으며 자신의 작업을 수행한다. <br> | ||
|
||
CSP는 Communicating Sequential Processes의 약자로 순차 프로세스 통신이라는 의미를 가지고 있다. <br> | ||
|
||
CSP 또한 Actor 머델과 비슷하다. 메시지를 전송하는 개체들에 초점을 맞추는 것이 아니라. **메시지가 전송되는 통로인 "채널" 자체에 초점을 맞춘다.** <br> | ||
|
||
|
||
CSP에서는 채널이 1급 시민의 자격을 갖는데, 미국에서 1급 시민이란 자유롭게 거주하고, 출입국 할 수 있는 시민을 의미한다. <br> | ||
프로그래밍에선 변수에 담을 수 있고, 함수의 인자로 전달할 수 있으며, 함수의 반환값으로 전달할 수 있으면 1급 시민이라고 부른다. <br> | ||
|
||
각 프로세스들은 하나의 Mail Box와 엮이는 대신, 채널은 독자적으로 어디서든 생성될 수 있고, 데이터를 쓰고 읽을 수 있으며, 프로세스 사이에 전달될 수 있다. <br> | ||
|
||
CSP는 제안된 지 오래된 개념이지만 Go에 적용되며 최근 다시 주목을 받고 있다. 이 글에서는 Clojure이라는 언어에서 Golang에 적용된 동시성 모델을 가져와 구현한 라이브러리를 예시로 설명한다. 따라서, 중간 중간 Go Block과 같은 용어들은 Go 언어에 대한 이야기가 아니라 Clojure에 대한 이야기임을 주의한다. <br> | ||
이제 Clojure의 채널과 Go Block을 이해하고, CSP에 대해 더 이해해보자. | ||
|
||
# 2.1 채널 | ||
채널은 데이터를 주고 받을 수 있는 일종의 Queue이고 버퍼이다. 1급 시민으로써의 채널은 이곳 저곳에서 생성될 수 있고, 고루틴 사이에서 전달될 수 있다. <br> | ||
그리고 채널에 대한 참조를 가지고 있는 모든 작업 주체들은 모두 Queue의 맨 뒤에 메시지를 추가할 수 있고, Queue의 맨 앞에서 메시지를 가져올 수 있다. 이러한 채널을 통해 스레드의 안전성을 제공해줄 수 있다. <br> <br> | ||
Actor에선 어땠는가? 특정한 Actor를 대상으로 메시지를 보내고, 해당 액터만 메시지를 읽을 수 있는 경우와 달리 CSP에서는 메시지를 보내는 개체와 받는 개체가 서로에 대해 전혀 알 필요가 없으므로 더 유연하다. <br> | ||
마치 1급 시민으로써 전달되는 Message Queue와 같다. <br> <br> | ||
기본적으로 CSP의 채널은 버퍼가 없는 unbuffered이다. 따로 저장공간이 없기 때문에, 채널에 메시지를 써 넣는 동작은 다른 작업 주체가 채널에서 메시지를 읽을 때까지 Blocking된다. (Java의 SynchronousQueue와 유사) <br> | ||
|
||
Go에 적용된 채널엔 버퍼 사이즈를 전달함으로써 버퍼가 내장된 버퍼 채널도 만들 수 있다. TODO : 버퍼드 채널은 어떻게 동작하는지? <br> <br> | ||
|
||
<!-- 만약 채널이 닫혀있다면 쓰여지는 메시지를 조용히 버리고, 채널을 읽을 때는 null을 리턴한다. (nil) 그렇다고 null 자체를 넣을 수는 없다. 에러를 반환한다. --> | ||
|
||
## 2.1.1 채널의 활용 | ||
이러한 채널 덕분에 복잡한 콜백 함수를 작성할 필요가 없다. 절차지향적으로 단순히 채널에 값을 써 넣는 코드를 작성하고, 그 뒤에 어떤 일이 일어나야 할지 코드를 기술하는 것 만으로 아주 짧고 간결하게 콜백 작성이 가능하다. <br> | ||
(TODO : Go로 작성된 채널로 예시 만들어보자.) | ||
|
||
|
||
## 2.1.2 다른 종류의 버퍼들 | ||
원한다면 다른 동작을 갖는 버퍼를 선택할 수도 있다. | ||
1. dropping buffer : 버퍼가 꽉찬 이후 들어오는 메시지를 버린다 | ||
2. sliding buffer : 버퍼가 꽉찬 이후 메시지가 들어오면 오래된 것 부터 버린다. | ||
3. 단, 크기가 동적으로 커지는 버퍼는 없다. 데이터가 무한히 쌓일 수도 있기 때문이다. 이러한 상황은 예상 하기 어려운, 그리고 발견하고 대처하기도 어려운 미지의 문제들을 발생시킬 수도 있기 때문에 제고오디지 않는다. Actor 모델의 Erlnag에서 시스템을 붕괴하도록 만드는 방법 중 하나는 어떤 액터의 Mail Box를 꽉 차게 만드는 것이다. | ||
|
||
|
||
|
||
|
||
## 2.1.3 버퍼 채널 | ||
왜 무버퍼 채널이 채널이 내장된 버퍼보다 더 많이 사용될까? | ||
|
||
버퍼 채널이 사용되어 좋은 경우도 분명 있으나, 주의가 필요하다 | ||
|
||
## 2.2 Go Block | ||
Go Block이란 여러 개의 동시적인 작업이 제한된 스레드 풀을 이용해 효율적으로 동작하도록 허용한다. <br> | ||
|
||
스레드와 달리 고 블록은 값이 싸기 떄문에 자원을 소진시키지 않으면서도 수많은 고 블록을 원하는 만큼 만들 수 있다. <br> | ||
|
||
스레드도 무겁다. OS시간에 스레드를 배울 때는 그 가벼움과 멋짐에 심취했다. 하나의 Process 안에서 여러개의 독자적인 실행 흐름을 가지고, 가벼운 Context Switching이 가능했다. 하지만 스레드 풀을 사용하더라도 현대 시스템에선 이런 Thread도 무겁다! <br> | ||
|
||
시대가 발전하면서 우리는 하나의 거대한 어플리케이션 보다는 작은 여러개의 애플리케이션의 합으로 하나의 서비스를 운영하는 경우가 많아졌다. 또한 많은 기업들이 공개 API를 제공하기 시작했다. <br> | ||
이러한 변화로 서비스들은 작아진 대신 Application과 네트워크 통신은 기하급수적으로 늘어났다. 이제 순수하게 자신의 단일 Application에서 고객의 요청을 모두 처리해 결과를 보내주는 서비스는 거의 없다. 여러 Applicatino과 소통한 결과를 보내주는, Mash Up된 서비스들이 주류를 차지하게 되었다. <br> | ||
|
||
결국 이러한 시대의 흐름에 따라 외부 시스템 I/O가 매우 많아졌고, 이 I/O는 스레드를 잠들게 한다! | ||
|
||
스레드 풀은 훌륭한 기술이지만, 오래동안 Blocking 되는 스레드들이 많아지면서 사실상 스레드 풀을 사용하는 장점이 사라졌다. 우리는 다른 방식이 필요했다. <br> | ||
|
||
이러한 상황을 피하기 위해 Event-Driven한 설계를 가져갈 수도 있다. 하지만 이런 방식은 자연스럽게 흐름을 읽는 것을 방해해 이해하기 힘들게 만들고, 전역적으로 관리되는 데이터들, 전역적인 상태들이 남용될 수 있다. <br> | ||
이러한 공유되는 상태들은 동시성 문제를 유발할 수 있다. <br> | ||
|
||
Go block은 이 방식에서 장점만을 취한다. | ||
1. Event Driven한 code가 가진 효율성을 갖고, | ||
2. 코드의 구조와 가독성을 해치지 않는다. | ||
|
||
**눈에 보이지 않는 배후에서 순차적인 코드를 사건 중심으로 재작성한다.** | ||
|
||
### 2.2.1 Go Block의 스레드 제어 역전 | ||
|
||
Go Block 내부에서 작성된 코드는 하나의 상태기계로 치환되는데, 채널에서 값을 읽거나 써 넣을 때 블로킹 되는 대신 자신이 쥐고 있는 스레드에 대한 통제권을 포기하고, 구척에 주차 (Park) 시켜둔다. 다음 실행 순서ㄷ가 돌아오면 상태를 전이 시키고 원래 하던 일을 다른 스레드에서 수행한다. 굳이 스레드가 Sleep 되는 동안 계속 쥐고 있는 것이 아니라 다른 스레드를 사용하는 것이다. 마치 Go의 프로세서가 고루틴을 다루는 방식이랑 비슷한 것 같다. <br> | ||
|
||
## 2.3 Actor와 CSP | ||
엑터와 CSP는 매우 유사하다. 둘 다 독립적이고 동시에 실행되는 업무를 생성해 서로 메시지를 주고 받을 수 있게 한다. <br> | ||
하지만 둘은 서로 다른 부분을 강조한다. | ||
|
||
|
||
1. Actor는 실행 단위가 Mail Box와 같하게 결부되어 있다. 생산자와 소비자를 결합한다. CSP는 유연하게 채널을 생성하고 주고 받을 수 있다. | ||
2. CSP는 더 쉬운 콜백 활용이 가능하게 해준다. | ||
3. Actor가 분산과 장애 허용에 더 유연하다. 데드락에 걸릴 가능성이 있고, 병렬성에 대한 직접적인 지원이 없다. <br> **액터 커뮤니티는 분산과 장애 허용에, CSP 커뮤니티는 효율성과 표현성에 초점을 맞추고 있다.** |