빠른 결론

  • goroutine, 자바 Thread, OS Thread 모두 동시성(Concurrent) 처리를 구현한다.
  • 이들의 차이를 세가지 요소 (메모리 소비 / 설치와 철거 비용 / Context Switching 비용)의 관점에서 볼 수 있다.
  • 고루틴은 메모리와 소비, 설치와 철거 비용, 컨텍스트 스위칭 비용이 자바 스레드에 비해 저렴하다.
  • Thread는 유저 레벨 스레드 / 커널 레벨 스레드로 볼 수 있다.
  • 자바의 Thread는 유저 레벨 스레드이지만 커널 스레드로 1:1 매핑되는 방식으로 동작한다.

내용

동시성 프로그래밍을 다루기 위해서 Go는 고루틴, 자바는 쓰레드를 활용합니다. 고루틴의 개념을 빠르게 이해하기 위해서는 가벼운 스레드라고 이해할 수 있습니다.

관리

고루틴은 기본적으로 Go 런타임이 자체 관리합니다. Go 런타임 상에서 관리되는 작업 단위인 여러 Goroutine은 OS 쓰레드와 1:1 매핑되지 않고, 훨씬 적은 OS 쓰레드를 사용합니다.

하지만 자바의 스레드는 OS의 커널 스레드와 1:1 매핑됩니다.(초기의 JVM에서는 JVM이 직접 스케줄링 해 줬었지만, 현재는 OS의 스케줄링 정책을 따릅니다).

User Level 스레드 : Kernel Level 스레드

  1. N:1방식 (여러 user-level 스레드가 하나의 OS 스레드 위에서 돌아갑니다.)
  • 컨텍스트 스위칭이 빠르지만, 멀티코어를 활용할 수 없습니다.
  1. 1:1 자바가 사용하는 방식으로 1개의 스레드는 1개의 OS 스레드와 일치합니다.
  • 멀티코어를 활용할 수 있지만, 컨텍스트 스위치 속도가 느립니다.

3. M:N 여러개의 OS스레드 위에 여러개의 흐름을 갖습니다.

  • 구현이 어렵지만, 컨텍스트 스위치 속도도 빠르고 멀티코어도 활용할 수 있습니다.

결국 Go는 M:N 모델을 택하고, 구현의 어려움을 언어 차원에서 구현하여 제공함으로 해결했습니다.

메모리

고루틴의 생성에는 2kb의 스택공간만 필요로 합니다.

반면 쓰레드는 1Mb(500 * 2Kb)로 시작합니다.

웹 서버를 만든다고 할 때, 요청당 1개의 스레드를 만들게 된다면 스레드의 경우 OutOfMemoryError를 만나게 되거나, 메모리 공간을 넘어 가상 메모리에 페이징 되면서 성능 저하를 겪습니다.

설치와 철거

쓰레드는 OS로 부터 리소스를 요청하고 작업이 끝나면 리소스를 돌려주기 때문에 설치, 철거 비용이 큽니다. 이 문제의 해결방법 중 하나로 쓰레드 풀을 사용합니다.

고루틴은 런타임에서 설치(생성), 철거(파괴)작업 비용이 작습니다. 그렇기 때문에 Go는 고루틴들의 관리를 따로 지원하지 않습니다.

Context Switching 비용

고루틴은 교체가 일어날 때 3개의 레지스터(PC, SP, DX)만 저장,복원됩니다.

하지만 쓰레드는 16개의 범용 레지스터, 16개의 XMM 레지스터, 16개의 AVX 레지스터 등을 저장,복원해야 하기에 비용이 많이 듭니다.

  • 고루틴의 Context 스위칭은 다음의 경우 이루어집니다.

(unbuffered 채널에 접근할 때, 시스템 I/O가 발생할 때, 메모리가 할당되었을 때, time.Sleep()코드가 실행될 때, runtime.Gosched()가 실행될때)

 

출처 및 참조

https://blog.nindalf.com/posts/how-goroutines-work/

https://medium.com/@unmeshvjoshi/how-java-thread-maps-to-os-thread-e280a9fb2e06

https://brownbears.tistory.com/313

http://tleyden.github.io/blog/2014/10/30/goroutines-vs-threads/

'Developer > Langauge' 카테고리의 다른 글

GC (Garbage Collection)에 대해.  (0) 2020.11.04

GC란?

Garbage Collection는 JVM의 Heap영역에서 사용하지 않는 객체들을 제거하는 프로세스를 지칭한다.

 

어떤 알고리즘으로 작동하는가?

GC는 Mark & Sweep (+Compact) 알고리즘으로 작동한다. 사용하지 않는 객체를 Marking하고, 추후에 이를 Sweeping(쓸어버림)한다. 그럼 중간 중간 빈 공간이 생기는데 몇몇 GC들은 메모리 단편화를 막기위해 이를 Compact해주기도 한다.

 

Unreachable, Reachable?

Mark할 객체의 기준을 어떻게 정하는가? 사용하지 않는 객체들을 Unreachable하다고 얘기하는데, 해당 객체를 정하는 기준은 GC Root로 부터 해당 객체를 참조하고 있는지 확인한다. GC Root는 Stack, Method Area에 있는 지역변수값등을 얘기하고, Heap의 또다른 객체로 부터 이를 참조하는지도 확인한다.

 

상세 알고리즘 동작 방식은? *(Major GC, Minor GC, Young Generation, Old Generation)

java에서는 갓 생성된 객체를 Eden이라는 메모리 영역에 올리는데 해당 영역이 꽉차면 minor GC가 실행되면서 사용하지 않는 객체(unreachable한 객체)를 제거하고, 살아남은 객체들을 Survivor영역에 올린다. Survivor 영역은 0, 1로 나뉘고 한 phase당 하나만 사용된다. Minor GC는 Eden + Survivor(0 or 1)을 살피며 살아남은 객체를 현재 사용되지 않는 Survivor영역으로 옮긴다. 이렇게 옮기면서 각 객체들의 age를 올려준다.

특정 age 임계점이 되면 이를 Old generation 영역으로 옮긴다. Old generation영역도 꽉차면 Major GC가 실행되면서 사용하지 않는 객체들을 제거한다.

 

위와 같은 알고리즘(Young/Old 분리) 동작 방식을 취하게 된 계기는? (GC 설계의 두가지 전제)

1. 새롭게 생성된 객체들의 대부분은 금방 사용하지 않게 된다. (=> Young Generation에 한해서 Minor GC를 설계하게 된 계기)

2. 오래된 객체가 새로운 객체를 참조하는 일은 드물게 일어난다.

 

Stop- The - World

GC가 실행되는 동안 다른 스레드가 모두 멈추는데, 이를 Stop-The-World라고 한다.

 

GC의 종류는?

Serial GC - 싱글 스레드로 GC를 진행한다. 그만큼 Stop-The-World시간이 길다.

Parallel GC - Young Generation 처리를 병렬로 처리한다.

Parallel Old GC - Old Generation'도' 병렬로 처리한다.

CMS GC - GC를 처리하는 스레드는 하나, 하지만 다른 스레드는 멈추지 않고 프로그램을 진행한다. Compact과정이 없다.

G1 GC - CMS를 발전시킴. Heap 영역을 Region영역으로 나누어 Garbage만 있는 Region을 우선적으로 탐색하여 수거한다. Java9의 default GC.

+ Recent posts