목적: 메모리 오류를 방지하는 동시성 프로그래밍
동시성 프로그래밍에서의 위험 요소: Race condition
및 Deadlock
Rust
언어의 해결 방안: move-by-default
, ownership
, lifetime
=
연산자는 변수의ownership
(소유권)을 이동시킴- 소유권이 없는 변수는 사용 불가
- 변수가 정의된 scope를 벗어나면 자동 해제(
lifetime
소멸) - 역시 scope 밖에서 사용 불가
- 모든 것이 이동이면 불편하므로
ownership borrowing
(소유권 대여) 가능 - &(
Read-only
)를 여러 개 빌리거나 &mut(Read-Write
)를 한 개 빌릴 수 있음 - 대여된 ownership 반환 전까지는 원본 변수 사용 불가
- 목적: Race condition 방지
- 응용: 반복자 무효화(
iterator invalidation
?) 방지, 뮤텍스 유출 방지 등 - 이러한 작업들이 모두 컴파일 타임에 이루어지므로(zero-cost abstraction 등 포함) 성능 손해가 없음
Rust
의 장점:
C++
와 동등/이상 수준의 zero-cost 추상화- GC 없음(모든 메모리 관리는 Compile-time에 처리)
- Mozilla, Samsung의 지원
- Race condition, Dangling pointer 등의 메모리 오류를 컴파일 상황에서 방지
Rust
의 단점:
- 모든 메모리 오류를 예방할 수는 없음(특히 Deadlock)
- 높은 난이도(
C++
보다는 나은 것으로 알려짐) - 아름답지 않은(Not pythonic) 코드(기호로 점철됨)
Go
의 동시성 지원: Goroutine
이 핵심
Erlang
에서 영향을 받은 모델- Go runtime에서 자체 관리하는 경량 마이크로스레드
- 6KB 정도의 Stack 메모리만 할당(수천 개를 생성해도 부하 없음)
Go
의 특징
- C-family 문법(struct, 함수 선언 등) + Pythonic한 문법(세미콜론 불필요, 한 번에 여러 변수 할당 등)
- 덕 타이핑 기반의 OOP(struct에 함수를 붙여서 사용, 상속 X, 인터페이스 기반)
Goroutine
Channel
- Unbuffered: 동기, Buffered: 버퍼가 찰 때까지 비동기
- 자료형에 send 전용/receive 전용 명시 가능
First-In-First-Out
- 명시적 동기화에 이용(
chan struct{}
등으로 signal 전달 등) C
와의 접착성이 좋음(cgo
)- 매우 방대한 툴(
GoFmt
,pprof
,heapdump
,race detector
) - 다양한 디버깅 툴(
pprof
,heapdump
,tracer
,race detector
)이 기본 제공되나 디버깅이 어렵다는 평가가 있음(글쎄?) - GC 기반(
Go 1.5
부터 성능 개선, 최악 지연 시간이<10ms
) - 매우 빠른 컴파일 속도
Go
의 동시성 모델
- 고전적 방법(
Mutex
):sync.Mutex
,sync.RWMutex
사용 - 장점: 확실한 상호 배제로
Race condition
방지 - 단점: Deadlock의 위험(두 번 Lock하기, 서로 맞물리는 Lock 의존성) (
Dining philosophers
문제) - Gophers' method(Go Concurrency Patterns, Advanced Go Concurrency Patterns, http://blog.golang.org/context, http://blog.golang.org/pipelines):
Channel
을 이용한 명시적 동기화 - 모든
Goroutine
은 자기에게 할당된 변수만 접근, 상대 간섭은Channel
을 이용 - 다른
Goroutine
에 접근하려면 클로저(func() {}
)를Channel
로 전달 sync.WaitGroup
또는chan struct{}
를 활용해 다른Goroutine
들과 동기화- 장점:
Mutex
를 사용하지 않으므로Mutex
에 의한 Deadlock 위험이 없다. 제어 흐름을 간단히 표현 가능하다 - 단점: 채널 남발 시 비동기 전송으로 인해 역시 Deadlock 가능성이 있음
- Rust의 난해함을 해결(더 쉬운 언어)
- Rust보다 간결한 문법(Go-Style?)
- Go/Rust에서 해결하지 못한 Deadlock 이슈 해결
- Rust의 Ownership 모델 차용?(Race 이슈 해결)
- Rust의
block is an expression
차용 - 속도/안정성 보다는 Deadlock 해결을 위한 Concept 제시가 주된 목적
- No Mutex(컴파일타임 추론)
- No Class(Go와 같은 구조체에 대한 함수 정리만을 지원)
- Buzzer(Go의 channel broadcasting과 유사)
RunForm
- 변수
Import
/Export
- Block 스코프
- Move by default(bool, int, uint, float 제외)
- 정적 타입
runform R1 { // async runform 선언도 가능
int A = 9 // 기본값
int B = 7
int NeedDef // 선언 필요
// RunForm Field의 lifetime은 RunForm 내에서 계속 유지
} Form1 (arg1, arg2) { // arg1, arg2의 lifetime은 Form1 내부에서만 유지
// Do something
arg2 // arg2의 lifetime을 다음 Form으로 연장
} Form2 (arg1 = 3) { // arg1 기본값 설정
// Do something
} { // Anonymous Form(Form2 후 바로 실행, 연장하지 않은 Form2의 모든 lifetime 소멸)
// Do something
} Form3 { // 인자 없을 시 () 생략가능
// Do something
go Form1(1, 2)// Form1로 점프
// 따로 인자가 필요 없는 Form2는 go Form2와 같이 () 생략 가능
}
R1{NeedDef:3}(2,3)(3)() // R1 생성 및 R3까지 바로 실행
var a R1
a = R1{NeedDef: 3}(2, 3) // R1 생성 후 Form1까지 실행
if stat a == R1.Form1 { // Form1 구동이 끝났으므로 Form1 상태. 조건문은 참
// 이 코드는 실행됩니다
}
// Async RunForm의 경우 wait문을 이용해 Form의 구동이 끝날 때까지 대기 가능
// 만약 Sleep된 Form 이외의 모든 Form이 인자 명시가 필요 없는 상황이라면, ... 으로 모두 실행 가능(form batching)
// ex) R1{NeedDef: 3}(2,3)...
async runform R2 {
int A = 3 // 다른 Thread에서 A에 접근 시 Mutex를 잠근다(Lock).
} Form1 {
var aa int
aa = 3
export aa // aa는 Form1 Sleep 까지 Lock 상태이다.
} { // 이 시점에서 잠시 Form1은 Sleep 상태가 되며 A, aa의 Lock이 해제된다. aa의 lifetime은 소멸되지 않는다.
// Do something with aa
drop aa // aa는 이제 이 RunForm 내에서 필요하지 않음을 선언한다. aa의 Lock이 다시 해제된다. 이 아래 코드 및 Form에서는 aa를 사용할 수 없다. 다시 사용하려면 import block을 사용한다.
}
// 주의: Re-export로 중복 Mutex 생성을 방지하기 위해, export가 들어간 Form에는 goto 사용이 제한된다.
var a R2
a = R2{}
import a.aa { // 여러 개인 경우 import (aa, bb) {}, aa가 아직 export되지 않았으면 export될 때까지 대기한다.
// aa는 이제 Lock되었다.
// Do something with aa
} // aa의 Lock은 다시 해제된다
// Block은 Expression으로 작용하므로 Rust와 같이 값을 돌려줄 수도 있다.
var g1 rungroup(R2) // R2는 무조건 Async이여야 한다.
g1.New(R2{})(2)... // .New() 이후는 기존 RunForm 실행과 동일
각 RunGroup은 하나의 Thread를 할당받고, 이 RunGroup 내에서 Async RunForm들이 구동된다. Form Sleep 상태에서는 다른 RunForm으로 실행 상태가 yield될 수 있다.
- 프로그래머가 .Lock(), .Unlock()을 사용하지 않고도 Multi-Thread 상황에서 변수 접근을 처리하는 방법을 제공
- Form Sleep 상태에서 자동적으로 Unlock되고, Form 내부에서는 drop하지 않는 이상 계속 Lock을 가지고 있음
- 변수의 Lock을 release하기 위해서 짧은 Anonymous Form들을 이어붙여 자주 Sleep시키는 것을 장려
- function, class, struct의 융합
- function은 Field가 없는 RunForm, struct는 Form이 없는 RunForm이 됨
- 프로그래머가 비슷한/동일한 작업을 묶어서 비동기로 처리해야 하는 경우(세션 처리, 여러 DB로 쿼리 등)를 만났을 때, RunGroup 사용을 장려
- 각 RunGroup은 하나씩 Thread를 받고, 각 Form은 Sleep 전까지는 yield되지 않으므로 여러 RunForm 간의 흐름을 보장 가능