고루틴(goRoutine)
쓰레드
먼저 쓰레드는 실행 흐름을 의미.
프로그램은 사실 기계어 명령의 다발인데, 이게 로드되면 메모리로 올라가게 된다.
CPU는 이 명령어를 실행하는 주체이고 그냥 연산해서 결과를 보내주는 녀석이다.
그래서 이제 원래는 하나의 CPU가 이 명령어의 다발(쓰레드)를 실행해 주었다.
멀티 쓰레드
근데 이제 멀티쓰레드 어쩌고를 들어본적이 있을텐데, 그거는 코어가 빠르게 쓰레드를 교체하게 된다. (실행 흐름이 여러개가 돌아감)
이게 어떻게 가능할까?
- 각 쓰레드는 자신만의 IP포인트가 있다.
- CPU가 각 IP포인트에 있는 명령을 수행한다.
- 그러다가 다음 쓰레드의 IP포인트에서부터 증가하면서 수행한다.
- 그러고 또 다음 쓰레드를 찾아간다.
그니까 사실 동시에라기 보다는 각 쓰레드의 명령을 수행하다가 다음 쓰레드를 수행하고, 다시 이전 쓰레드에서는 IP포인트를 통해 진행해 갈 명령을 찾는 방식이라고 생각하면 된다.
-> 만약에 코어가 2개다! 그러면 걔들이 또 알아서 번갈아가면서 할거다.
참고로 이거 CPU는 걍 계산만 해주는 녀석이고, OS가 멀티쓰레딩을 해주는것이다(저기 위해서 IP포인트 찾고 쓰레드 찾아가고 등등~)
컨텍스트 스위칭
근데 이거 보면 사실 동시에는 아니고 하나씩 바꿔서 하는거다.
Heap은 뭐 메모리를 공유한다고 치는데, Stack의 경우 그게 안된다. 즉 쓰레드별로 따로따로 있다.
즉, 오히려 생각하면 쓰레드를 바꿀 때에 메모리를 변경해주는(Stack메모리를)비용이 들어가는 것이다.
이를 컨텍스트 스위칭(Context Switching)이라고 한다.
고루틴(goRoutine)
Go 에서 만든 경량 쓰레드
go는 최소 1개의 고루틴을 갖는다(메인 고루틴)
그래서 고루틴랑 쓰레드의 차이가 뭘까??
고루틴은 OS쓰레드를 이용하는 경량 쓰레드이다.
그니까 고루틴은 쓰레드랑 다르고, 고루틴은 쓰레드를 이용하는거다. 그리고 가볍다.
뭐가 어떻게 가벼운데...?
- CPU 코어가 존재한다
- 이 코어에 OS쓰레드 하나를 연동한다(하나의 코어는 하나의 OS쓰레드만을 가진다.)
- 그 쓰레드에 고루틴 하나씩 붙인다.
그러니까 고루틴은 쓰레드가 실행하도록 붙여지는 것이고, 대기하는 고루틴이 있다면 기존 쓰레드가 대기중이 되면 거기서 실행되게 된다.
대기
그러면 쓰레드는 대기를 많이 할까??
ㅇㅇ 많이함.
- 파일 읽기 / 쓰기
- 이 때에 쓰레드는 대기한다.
- 왜냐면 실제로 쓰레드가 이거를 하지 않고, OS에 요청하기 때문이다.(시스템 콜 : 시스템한테 부탁하는거임)
- 그리고 OS가 파일 읽기/쓰기를 한 뒤에 알려준다.
- 그리고 그 OS가 파일 읽기/쓰기를 하는 동안 쓰레드는 대기하게 된다.
- 네트워크 읽기 / 쓰기
- 패킷 전송같은거
- 이것도 시스템 콜 -> OS가 담당함
대충 이런 식으로 얘가 대기가 많이 되는데, 이 때 걍 고루틴을 스위칭한다.
정리하자면
OS쓰레드 개수는 코어의 개수만큼만 만들고, 고루틴을 대기가 있을 때마다 교체하면서 하는거다.
그래서 이게 위에서 말했듯이 쓰레드를 코어보다 많이 만드는게 아니니까(코어가 쓰레드를 스위칭하는 경우가 없으니까) OS단의 컨텍스트 스위칭이 일어나지는 않는다(물론 고루틴이 교체되는것도 컨텍스트 스위칭임ㅇㅇ)
그 고루틴을 바꾸는 컨텍스트 스위칭 비용이 >>>>> 쓰레드를 변경하는 컨텍스트 스위칭 비용 이다. 엄청 적다고 한다. (고루틴의 stack size가 처음에 좀 많이 작은편임 -> 문맥 자체가 작아서 스위칭 비용이 작다) -> 그래서 경량임
동시성 프로그래밍
근데 만약에 위에서 생각할 때에
코어 3개 -> OS쓰레드 3개 -> 고루틴 3개 라고 가정해 보자
그러면 얘들은 스위칭 없이 셋이 동시에 좌라라라락 될거다.
여기서 얘들이 같은 Heap에 접근한다면?(전역변수에 동시 접근!!) 당연히 문제가 생길것이다
해결법 - 뮤텍스 사용 O
가장 간단한 방법은 Lock을 사용하는 것이고(하나의 고루틴에서만 메모리 자원에 접근하도록 하는것) 이를 뮤텍스(Mutual Exclusion)이라고 한다.
대충 이거의 pessimistic lock이나 이거의 synchronize같은걸 생각해주면 될 것 같다.
근데 뮤텍스의 단점은 뭐 다 알겠지만
- 동시성 프로그래밍으로 인한 성능 향상을 얻을 수 없다.
- 동시에 하는게 병렬적으로 처리해서 빠르게 하려는건데 여러 고루틴이 진행되는데도 불구하고 뮤텍스가 걸려서 성능 이점이 없어짐
- 과도한 락킹으로 성능이 하락될 수 있다.
- 락 자체도 성능을 먹는다(락 획득 - 10^-3s, 락 반납 등..)
- 데드락이 발생할 수 있다.
- 이거의 deadlock을 참고하면 쉬운데, 각 고루틴들이 행동을 하기 위해서는 특정 순서가 있는데, 그거를 다른쪽이 선점하면 데드락이 생겨버릴 수 있는 것이다.
그래서 이거를 쓰면 성능상 이점이 크지 않고, 오히려 문제가 발생할 수 있는 것이다!!
해결법 - 뮤텍스 사용 X
영역을 나누는 방법
각 고루틴들에게 Heap의 영역을 나누어 주고, 그 영역 내에서만 작업을 진행하도록 하는 것이다.
역할을 나누는 방법
각 고루틴들에게 역할을 나누어 주고, 그 역할만 하도록 하는 것이다.
여기 쓰이는게 바로 channel이다.
'이론 정리 > Golang' 카테고리의 다른 글
Go의 GC에 대해 (feat. java) (1) | 2022.10.31 |
---|---|
Golang의 포인터 (0) | 2022.07.24 |
Golang의 구조체 (0) | 2022.07.24 |
Golang의 배열 (0) | 2022.07.24 |
Golang의 기초 (0) | 2022.07.24 |