Go의 동시성에 대한 심층 분석
동시성 측면에서 가장 강력한 프로그래밍 언어
StackOverflow 개발자 설문 조사 및 TIOBE 인덱스에 따르면 Go(또는 Golang)는 최근 몇 년 동안 특히 인프라 자동화 작업을 하는 백엔드 개발자와 DevOps 팀 사이에서 더 많은 관심을 받았습니다. 이것이 Go와 동시성을 다루는 영리한 방법에 대해 이야기하기에 충분한 이유입니다.
Go는 동시성에 대한 최고 수준의 지원 또는 프로그램이 한 번에 여러 가지를 처리할 수 있는 기능으로 유명합니다. 코드 동시 실행은 컴퓨터가 단일 코드 스트림을 더 빠르게 실행하는 것에서 동시에 더 많은 스트림을 실행하는 것으로 이동함에 따라 프로그래밍에서 더 중요한 부분이 되고 있습니다.
프로그래머는 프로그램의 각 부분이 다른 부분과 독립적으로 실행될 수 있도록 동시에 실행되도록 설계하여 프로그램을 더 빠르게 실행할 수 있습니다. Go의 세 가지 기능인 고루틴, 채널 및 선택은 함께 사용할 때 동시성을 더 쉽게 만듭니다.
고루틴은 프로그램에서 동시 실행 코드의 문제를 해결하고 채널은 동시에 실행되는 코드 간의 안전한 통신 문제를 해결합니다.
고루틴은 의심할 여지 없이 Go의 최고의 기능 중 하나입니다. OS 스레드와 달리 매우 가볍지만 컨텍스트 전환의 오버헤드를 최소화하면서 수백 개의 고루틴을 OS 스레드에 다중화할 수 있습니다(Go에는 런타임 스케줄러가 있습니다). 간단히 말해서 고루틴은 스레드에 대한 가볍고 저렴한 추상화입니다.
그러나 Go의 동시성 접근 방식은 내부에서 어떻게 작동합니까? 오늘은 이것을 여러분에게 설명하려고 합니다. 이 기사는 이러한 엔터티 자체보다 Go의 동시성 엔터티의 오케스트레이션에 더 중점을 둡니다.
말하자면, 그 작업은 하나 이상의 프로세서(P)에서 실행되는 여러 작업자 OS 스레드(M)에 실행 가능한 고루틴(G)을 배포하는 것입니다. 프로세서가 여러 스레드를 처리하고 있습니다. 스레드는 여러 고루틴을 처리합니다. 프로세서는 하드웨어에 따라 달라지며 프로세서 수는 CPU 코어 수에 따라 설정됩니다.

- G = 고루틴
- 중 = OS 스레드
- 피 = 프로세서
새로운 고루틴이 생성되거나 기존 고루틴이 실행 가능하게 되면 현재 프로세서의 실행 가능한 고루틴 목록에 푸시됩니다. 프로세서가 고루틴 실행을 마치면 먼저 실행 가능한 고루틴 목록에서 고루틴을 꺼내려고 시도합니다. 목록이 비어 있으면 프로세서는 임의의 프로세서를 선택하고 실행 가능한 고루틴의 절반을 훔치려고 시도합니다.
고루틴은 다른 기능과 동시에 실행되는 기능입니다. 고루틴은 OS 스레드 위에 있는 가벼운 스레드로 생각할 수 있습니다. 스레드와 비교할 때 고루틴을 만드는 비용은 적습니다. 따라서 Go 애플리케이션은 수천 개의 고루틴을 동시에 실행하는 것이 일반적입니다.
고루틴은 더 적은 수의 OS 스레드로 다중화됩니다. 수천 개의 고루틴이 있는 프로그램에는 스레드가 하나만 있을 수 있습니다. 해당 스레드의 고루틴이 사용자 입력을 기다리고 있다고 말하면 다른 OS 스레드가 생성되거나 파킹된(유휴) 스레드가 당겨지고 나머지 고루틴은 생성되거나 파킹되지 않은 OS 스레드로 이동됩니다. 이 모든 것은 Go의 런타임 스케줄러에 의해 처리됩니다. 고루틴에는 실행, 실행 가능, 실행 불가의 세 가지 상태가 있습니다.
고루틴 대 스레드
Go가 이미 사용하는 것처럼 간단한 OS 스레드를 사용하지 않는 이유는 무엇입니까? 그것은 정당한 질문입니다. 위에서 언급했듯이 고루틴은 이미 OS 스레드 위에서 실행되고 있습니다. 그러나 차이점은 여러 고루틴이 단일 OS 스레드에서 실행된다는 것입니다.
고루틴 생성에는 많은 메모리가 필요하지 않으며 2kB의 스택 공간만 필요합니다. 그들은 필요에 따라 힙 스토리지를 할당하고 해제함으로써 성장합니다. 스레드는 한 스레드의 메모리와 다른 스레드의 메모리 사이에서 보호 역할을 하는 보호 페이지라는 메모리 영역과 함께 훨씬 더 큰 공간에서 시작합니다.
고루틴은 런타임에 쉽게 생성 및 소멸되지만 스레드는 OS에서 리소스를 요청하고 완료되면 반환해야 하는 설정 및 해제 비용이 많이 듭니다.
런타임에는 모든 고루틴이 다중화되는 몇 개의 스레드가 할당됩니다. 어느 시점에서든 각 스레드는 하나의 고루틴을 실행합니다. 해당 고루틴이 차단되면(함수 호출, 시스템 호출, 네트워크 호출 등) 해당 스레드에서 대신 실행할 다른 고루틴으로 교체됩니다.
요약하면, Go는 고루틴과 쓰레드를 사용하고 있으며, 둘 다 기능을 동시에 실행하는 조합에서 매우 중요합니다. 그러나 Go가 고루틴을 사용한다는 사실은 Go를 처음 보는 것보다 훨씬 더 훌륭한 프로그래밍 언어로 만듭니다.
고루틴 큐
Go는 로컬 큐와 글로벌 큐의 두 가지 수준에서 고루틴을 관리합니다. 로컬 큐는 각 프로세서에 연결되지만 글로벌 큐는 공통입니다.
고루틴은 로컬 큐가 가득 찼을 때만 전역 큐에 들어가지 않고 Go가 스케줄러에 고루틴 목록을 삽입할 때도 푸시됩니다.
프로세서에 고루틴이 없으면 다음 규칙을 이 순서로 적용합니다.
- 자체 로컬 대기열에서 작업 가져오기
- 네트워크 폴러에서 작업 가져오기
- 다른 프로세서의 로컬 큐에서 작업 훔치기
- 전역 대기열에서 작업 가져오기
프로세서는 작업이 부족할 때 전역 대기열에서 작업을 가져올 수 있으므로 사용 가능한 첫 번째 P가 고루틴을 실행합니다. 이 동작은 고루틴이 다른 P에서 실행되는 이유를 설명하고 Go가 리소스가 비어 있을 때 다른 고루틴이 실행되도록 하여 시스템 호출을 최적화하는 방법을 보여줍니다.

이 다이어그램에서 P1에 고루틴이 부족함을 알 수 있습니다. 따라서 Go의 런타임 스케줄러는 다른 프로세서에서 고루틴을 가져옵니다. 다른 모든 프로세서 실행 대기열이 비어 있으면 netpoller에서 완료된 IO 요청(시스템 호출, 네트워크 요청)을 확인합니다. 이 넷폴러도 비어 있으면 프로세서는 전역 실행 대기열에서 고루틴을 가져오려고 시도합니다.
이 코드 조각에서 20개의 고루틴 함수를 만듭니다. 각각은 1초 동안 잠을 잔 다음 1e10(10,000,000,000)으로 계산합니다. env를 다음으로 설정하여 Go 스케줄러를 디버그해 보겠습니다. GODEBUG=schedtrace=1000
.
암호
결과
결과는 전역 대기열에 있는 고루틴의 수를 보여줍니다. runqueue
및 로컬 대기열(각각 P0
그리고 P1
) 괄호 안에 [5 8 3 0]
. 성장 속성에서 볼 수 있듯이 로컬 큐가 256개의 대기 고루틴에 도달하면 다음 큐가 전역 큐에 쌓이게 됩니다.
gomaxprocs
: 구성된 프로세서idleprocs
: 프로세서를 사용하지 않습니다. 고루틴 실행 중.threads
: 사용 중인 스레드입니다.idlethreads
: 쓰레드를 사용하지 않습니다.runqueue
: 글로벌 큐의 고루틴.[1 0 0 0]
: 각 프로세서의 로컬 실행 큐에 있는 고루틴.
idleprocs=1 threads=6 idlethreads=0 runqueue=0 [1 0 0 0]
idleprocs=2 threads=3 idlethreads=0 runqueue=0 [0 0 0 0]
idleprocs=4 threads=9 idlethreads=2 runqueue=0 [0 0 0 0]
idleprocs=0 threads=5 idlethreads=0 runqueue=0 [5 8 3 0]
idleprocs=4 threads=9 idlethreads=2 runqueue=0 [0 0 0 0]
idleprocs=0 threads=5 idlethreads=0 runqueue=8 [2 2 1 3]
idleprocs=4 threads=9 idlethreads=2 runqueue=0 [0 0 0 0]
idleprocs=0 threads=5 idlethreads=0 runqueue=10 [3 1 0 2]
idleprocs=4 threads=9 idlethreads=2 runqueue=0 [0 0 0 0]
idleprocs=0 threads=5 idlethreads=0 runqueue=9 [4 0 3 0]
idleprocs=4 threads=9 idlethreads=2 runqueue=0 [0 0 0 0]
idleprocs=0 threads=5 idlethreads=0 runqueue=10 [2 1 1 2]
idleprocs=4 threads=9 idlethreads=2 runqueue=0 [0 0 0 0]
idleprocs=0 threads=5 idlethreads=0 runqueue=6 [2 1 0 0]
Go의 동시성에 대한 제 글을 읽어주셔서 감사합니다. 나는 당신이 새로운 것을 배울 수 있기를 바랍니다.
건배!
'Coding' 카테고리의 다른 글
SpriteKit 및 GameplayKit을 사용하여 아케이드 모바일 게임을 만드는 방법 (0) | 2022.04.13 |
---|---|
좋은 API 문서를 작성하는 방법 (0) | 2022.04.12 |
속도, 보안 및 확장성 :: 모든걸 가질 순 없다. 중요한 두 가지는? (0) | 2022.04.10 |
Go에서 Kubernetes 인터페이스 애플리케이션 빌드, 테스트 및 자동화하는 방법 (0) | 2022.04.09 |
SceneKit에서 개체를 회전하는 3가지 방법 (0) | 2022.04.08 |
댓글