>백엔드 개발 >Golang >Go가 왜 그렇게 '빠른' 걸까요?

Go가 왜 그렇게 '빠른' 걸까요?

藏色散人
藏色散人앞으로
2020-03-04 09:34:312909검색

이 글에서는 매우 높은 동시성 성능을 달성하기 위한 Go 프로그램의 내부 스케줄러 구현 아키텍처(G-P-M 모델)를 주로 소개하고, 컴퓨팅 리소스 사용을 극대화하기 위해 Go 스케줄러가 스레드 차단 시나리오를 처리하는 방법을 소개합니다.

시스템을 더 빠르게 만드는 방법

정보 기술의 급속한 발전으로 단일 서버의 처리 능력이 점점 더 강력해지고 있어 프로그래밍 모델이 이전 직렬 모델에서 동시 모델로 업그레이드될 수밖에 없습니다.

동시성 모델에는 IO 멀티플렉싱, 다중 프로세스 및 다중 스레딩이 포함됩니다. 이러한 각 모델에는 고유한 장점과 단점이 있습니다. 대부분의 현대 복잡한 고동시성 아키텍처는 여러 모델과 함께 사용됩니다. 강점을 극대화하고 약점을 방지하는 시나리오.

멀티 스레딩은 가볍고 사용하기 쉽기 때문에 나중에 파생된 코루틴과 이를 기반으로 하는 기타 하위 제품을 포함하여 동시 프로그래밍에서 가장 자주 사용되는 동시성 모델이 되었습니다.

동시성 ≠ 병렬

동시성과 병렬성은 다릅니다.

단일 CPU 코어에서 스레드는 "동시에" 여러 작업을 실행하려는 목적을 달성하기 위해 시간 조각을 통해 작업 전환을 실현하거나 제어 권한을 포기합니다. 그러나 실제로는 언제든지 하나의 작업만 실행되고 다른 작업은 일부 알고리즘을 통해 대기열에 추가됩니다.

멀티 코어 CPU는 동일한 프로세스의 "다중 스레드"를 진정한 의미에서 동시에 실행할 수 있게 해줍니다.

프로세스, 스레드, 코루틴

프로세스: 프로세스는 시스템의 자원 할당의 기본 단위이며 독립적인 메모리 공간을 갖습니다.

스레드: 스레드는 CPU 스케줄링 및 디스패치의 기본 단위입니다. 스레드는 프로세스에 연결되며 각 스레드는 상위 프로세스의 리소스를 공유합니다.

코루틴: 코루틴은 사용자 모드의 경량 스레드입니다. 코루틴 간의 전환은 커널 오버헤드 없이 작업 컨텍스트를 저장하기만 하면 됩니다.

스레드 컨텍스트 전환

인터럽트 처리, 멀티태스킹, 사용자 모드 전환 및 기타 이유로 인해 CPU는 한 스레드에서 다른 스레드로 전환합니다. 전환 프로세스에서는 현재 프로세스의 상태를 저장하고 다른 프로세스의 상태를 복원해야 합니다. 프로세스.

컨텍스트 전환은 코어에서 스레드를 교환하는 데 많은 시간이 걸리기 때문에 비용이 많이 듭니다. 컨텍스트 전환 대기 시간은 다양한 요인에 따라 달라지며 범위는 50~100나노초입니다. 하드웨어가 코어당 나노초당 평균 12개의 명령을 실행한다는 점을 고려하면 컨텍스트 전환에는 대기 시간으로 인해 600~1200개의 명령이 소요될 수 있습니다. 실제로 컨텍스트 전환은 명령을 실행하는 데 많은 프로그램 시간을 소비합니다.

크로스 코어 컨텍스트 스위치가 있는 경우 CPU 캐시 오류가 발생할 수 있습니다. (CPU가 캐시에서 데이터에 액세스하는 데 드는 비용은 약 3~40클럭 주기이고, 메인 메모리에서 데이터에 액세스하는 데 드는 비용은 약 100~300클럭입니다.) 사이클 클록 사이클), 이 시나리오의 스위칭 비용은 더 비쌉니다.

Golang은 동시성을 위해 태어났습니다

2009년 공식 출시 이후 Golang은 매우 빠른 실행 속도와 효율적인 개발 효율성을 바탕으로 빠르게 시장 점유율을 차지했습니다. Golang은 언어 수준에서 동시성을 지원하고 경량 코루틴 Goroutine을 사용하여 프로그램의 동시 실행을 실현합니다.

고루틴은 매우 가벼우며 주로 다음 두 가지 측면에 반영됩니다.

컨텍스트 전환 비용은 적습니다. 고루틴 컨텍스트 전환에는 세 개의 레지스터(PC/SP/DX) 값 수정만 포함되며 대조 스레드 컨텍스트 전환에는 필요합니다. 모드 전환(사용자 모드에서 커널 모드로 전환) 및 16개 레지스터, PC, SP... 및 기타 레지스터 새로 고침이 포함됩니다.

낮은 메모리 사용량: 스레드 스택 공간은 일반적으로 2M, Goroutine 스택 공간은 최소 2K입니다.

Golang 프로그램 10w 수준의 고루틴 연산을 쉽게 지원할 수 있지만 스레드 수가 1k에 도달하면 메모리 사용량이 2G에 도달합니다.

Go 스케줄러 구현 메커니즘:

Go 프로그램은 스케줄러를 통해 고루틴이 커널 스레드에서 실행되도록 예약하지만 고루틴은 OS 스레드 M-Machine에 직접 바인딩되어 실행되지 않고 고루틴의 P 프로세서에 의해 실행됩니다. 커널 스레드 리소스를 얻기 위해 "중개자" 역할을 하는 스케줄러(논리 프로세서)입니다.

Go 스케줄러 모델은 일반적으로 G-P-M 모델이라고 합니다. 여기에는 G, P, M 및 Sched의 4가지 중요한 구조가 포함됩니다.

G: 각 고루틴은 G 구조에 해당합니다. , 상태 및 작업 기능, 재사용 가능.

G는 실행 주체가 아닙니다. 실행을 예약하려면 각 G를 P에 바인딩해야 합니다.

P: 프로세서는 논리 프로세서를 나타냅니다. G의 경우 P는 CPU 코어와 동일합니다. G는 P에 바인딩된 경우에만 예약할 수 있습니다. M의 경우 P는 메모리 할당 상태(mcache), 작업 큐(G) 등 관련 실행 환경(Context)을 제공합니다.

P 수는 시스템에서 병렬화할 수 있는 최대 G 수를 결정합니다(전제: 물리적 CPU 코어 수 >= P 수).

P 개수는 사용자가 설정한 GoMAXPROCS에 따라 결정되지만, GoMAXPROCS 설정이 아무리 커도 최대 P 개수는 256개입니다.

M: OS 커널 스레드 추상화인 머신은 실제로 계산을 수행하는 리소스를 나타냅니다. 유효한 P를 바인딩한 후 일정 루프에 들어가고 일정 루프의 메커니즘은 대략적으로 P의 로컬 대기열에서 나옵니다. 대기열에서 가져오기를 기다립니다.

M의 수는 가변적이며 Go Runtime에 의해 조정됩니다. 너무 많은 OS 스레드가 생성되어 시스템이 너무 많은 OS 스레드를 예약하는 것을 방지하기 위해 현재 기본 최대 제한은 10,000입니다.

M은 G 상태를 유지하지 않습니다. 이는 G가 M 전체에 예약되는 기반이 됩니다.

Sched: M과 G 및 스케줄러의 일부 상태 정보를 저장하는 대기열을 유지 관리하는 Go 스케줄러입니다.

스케줄러 주기 메커니즘은 대략적으로 다양한 큐와 P의 로컬 큐에서 G를 얻고, G의 실행 스택으로 전환하고 G의 함수를 실행하고, Goexit를 호출하여 정리하고 M으로 돌아가는 등의 작업입니다.

M, P, G의 관계를 이해하려면 벽돌을 움직이는 땅바닥 수레의 고전적인 모델을 통해 관계를 설명할 수 있습니다.

Go가 왜 그렇게 빠른 걸까요? #🎜 🎜#

고퍼의 임무는 건설 현장에 수많은 벽돌이 있고, 고퍼는 트롤리를 사용하여 벽돌을 불로 옮기는 것입니다. M은 사진 속 땅다람쥐, P는 자동차, G는 자동차에 설치된 벽돌이라고 볼 수 있다.

이제 세 사람의 관계를 파악했으니, 이제 다람쥐가 벽돌을 운반하는 방법에 집중해 보겠습니다.

프로세서(P):

사용자가 설정한 GoMAXPROCS 값을 기반으로 자동차 배치(P)를 생성합니다.

Goroutine(G):

Go 키워드는 고루틴을 만드는 데 사용됩니다. 이는 벽돌을 만든 다음(G) 이 벽돌을 변환(G )하는 것과 같습니다. 현재 자동차(P)에 탑승합니다.

머신(M):

두더지(M)는 외부에서 생성할 수 없습니다. 벽돌(G)이 너무 많고 두더지(M)가 너무 적습니다. 너무 바빠서 사용하지 않는 무료 자동차(P)가 있는 경우, 모든 자동차(P)가 다 사용될 때까지 다른 곳에서 고퍼(M)를 더 빌리십시오.

두더지(M)가 부족하여 다른 곳에서 빌려온 프로세스가 있습니다. 이 프로세스는 커널 스레드(M)를 생성하는 것입니다.

고퍼(M)는 카트(P) 없이는 벽돌을 운반할 수 없다는 점에 유의해야 합니다. 카트(P)의 수에 따라 작업할 수 있는 고퍼(M)의 수가 결정됩니다. 프로그램에서 이는 활성 스레드 수에 해당합니다.

P는 "병렬"로 실행될 수 있는 논리 프로세서를 나타냅니다. 각 P는 시스템 스레드 M에 할당되고 G는 Go 코루틴을 나타냅니다.

Go 스케줄러에는 GRQ(글로벌 실행 큐)와 LRQ(로컬 실행 큐)라는 두 가지 실행 큐가 있습니다.

각 P에는 P의 컨텍스트에서 실행하도록 할당된 고루틴을 관리하는 데 사용되는 LRQ가 있습니다. 이러한 고루틴은 차례로 P에 바인딩된 M에 의해 컨텍스트 전환됩니다. GRQ는 아직 P에 할당되지 않은 고루틴에 적용됩니다. Go가 왜 그렇게 빠른 걸까요?

위 그림에서 볼 수 있듯이 G의 개수는 M의 개수보다 훨씬 클 수 있습니다. 즉, Go 프로그램은 소수의 커널 수준 스레드를 사용하여 지원할 수 있습니다. 다수의 고루틴 동시성. 여러 고루틴은 사용자 수준의 컨텍스트 전환을 통해 커널 스레드 M의 컴퓨팅 자원을 공유하지만, 운영체제에 대한 스레드 컨텍스트 전환으로 인한 성능 손실은 없습니다.

스레드의 컴퓨팅 리소스를 최대한 활용하기 위해 Go 스케줄러는 다음과 같은 스케줄링 전략을 채택합니다.

작업 도용(작업 도용)

# 🎜🎜 #실제로는 어떤 고루틴은 빠르게 실행되고 어떤 고루틴은 느리게 실행된다는 것을 알고 있기 때문에 필연적으로 발생하게 될 문제는 바쁘면 죽고, 한가하면 Go는 절대 존재를 허용하지 않는다는 것입니다. P, 컴퓨팅 리소스를 잘 활용해야 합니다.

Go의 병렬 처리 기능을 향상하고 전반적인 처리 효율성을 높이기 위해 각 P 간의 G 작업이 불균형할 때 스케줄러는 다른 P의 GRQ 또는 LRQ에서 G 실행을 얻을 수 있도록 허용합니다.

차단 감소

실행 중인 고루틴이 스레드 M을 차단하면 어떻게 되나요? P의 LRQ에 있는 고루틴은 스케줄링을 얻을 수 없나요?

Blocking in Go는 주로 다음 4가지 시나리오로 나뉩니다.

시나리오 1: 고루틴이 원자성, 뮤텍스 또는 채널 작업 호출로 인해 차단되고 스케줄러가 현재 차단된 고루틴을 끄고 LRQ에서 다른 고루틴을 다시 예약하세요.

시나리오 2: 고루틴이 네트워크 요청 및 IO 작업으로 인해 차단되면 G와 M은 어떻게 되나요? 할?

Go 프로그램은 네트워크 요청 및 IO 작업을 처리하기 위해 네트워크 폴러(NetPoller)를 제공합니다. 백그라운드에서는 kqueue(MacOS), epoll(Linux) 또는 iocp(Windows)를 사용하여 다중 IO 로드 멀티플렉싱을 구현합니다.

NetPoller를 사용하여 네트워크 시스템 호출을 하면 스케줄러는 이러한 시스템 호출을 할 때 Goroutine이 M을 차단하는 것을 방지할 수 있습니다. 이를 통해 M은 새로운 M을 생성하지 않고도 P의 LRQ에서 다른 고루틴을 실행할 수 있습니다. 운영 체제의 예약 부하를 줄이는 데 도움이 됩니다.

다음 그림은 작동 방식을 보여줍니다. G1은 M에서 실행 중이고 LRQ에서 실행을 기다리는 3개의 고루틴이 있습니다. 네트워크 폴러가 유휴 상태이며 아무 작업도 하지 않습니다.

Go가 왜 그렇게 빠른 걸까요?

다음으로 G1은 네트워크 시스템 호출을 원하므로 네트워크 폴러로 이동하여 비동기 네트워크 시스템 호출을 처리합니다. 그러면 M은 LRQ에서 추가 고루틴을 실행할 수 있습니다. 이때 G2는 M으로 컨텍스트 전환됩니다.

Go가 왜 그렇게 빠른 걸까요?

마지막으로 비동기 네트워크 시스템 호출은 네트워크 폴러에 의해 완료되고 G1은 P의 LRQ로 다시 이동됩니다. G1이 M에서 컨텍스트 전환을 할 수 있게 되면 담당하는 Go 관련 코드가 다시 실행될 수 있습니다. 여기서 가장 큰 장점은 네트워크 시스템 호출을 수행하는 데 추가 M이 필요하지 않다는 것입니다. 네트워크 폴러는 항상 활성 이벤트 루프를 처리하는 시스템 스레드를 사용합니다.

Go가 왜 그렇게 빠른 걸까요?

이 호출 방법은 매우 복잡해 보입니다. 다행스럽게도 Go 언어는 런타임에서 이 "복잡성"을 숨깁니다. Go 개발자는 주의할 필요가 없습니다. 소켓이 비블록인지, 파일 설명자의 콜백을 직접 등록할 필요가 없는지, 각 연결에 해당하는 고루틴의 "블록 I/O" 메서드에서만 소켓을 처리하면 고루틴별을 실현할 수 있습니다. -connection 간단한 네트워크 프로그래밍 모드(그러나 고루틴 수가 많으면 스택 메모리 증가 및 스케줄러 부담 증가와 같은 추가 문제도 발생합니다).

사용자 계층에서 볼 수 있는 Goroutine의 "블록 소켓"은 실제로 비블록 소켓 + I/O 다중화 메커니즘을 통해 Go 런타임의 netpoller에 의해 "시뮬레이션"됩니다. Go의 넷 라이브러리는 정확히 이런 방식으로 구현됩니다.

시나리오 3: 일부 시스템 메소드를 호출할 때 시스템 메소드가 호출 시 차단되는 경우, 이 경우 네트워크 폴러(NetPoller)를 사용할 수 없으며 시스템 호출을 하는 고루틴이 차단됩니다. 현재의 엠.

동기 시스템 호출(예: 파일 I/O)로 인해 M이 차단되는 상황을 살펴보겠습니다. G1은 M1을 차단하기 위해 동기 시스템 호출을 수행합니다.

Go가 왜 그렇게 빠른 걸까요?

스케줄러가 개입한 후: G1으로 인해 M1이 차단되었음을 인식합니다. 이때 스케줄러는 M1을 P에서 분리하고 G1도 제거합니다. . 그러면 스케줄러는 P를 서비스하기 위해 새로운 M2를 도입합니다. 이때 LRQ에서 G2를 선택할 수 있고, M2에서는 Context Switch를 수행할 수 있다.

Go가 왜 그렇게 빠른 걸까요?

차단 시스템 호출이 완료된 후: G1은 LRQ로 다시 이동되고 P에 의해 다시 실행될 수 있습니다. 이런 일이 다시 발생하면 M1은 향후 재사용을 위해 따로 보관됩니다.

Go가 왜 그렇게 빠른 걸까요?

시나리오 4: 고루틴에서 절전 작업을 수행하면 M이 차단됩니다.

Go 프로그램의 백그라운드에는 모니터링 스레드 sysmon이 있습니다. 장기 실행되는 G 작업을 모니터링한 다음, 다른 고루틴이 선제적으로 실행할 수 있도록 탈취할 수 있는 식별자를 설정합니다.

이 고루틴이 다음에 함수 호출을 하는 한, 그것은 점유되고 장면도 보호될 것입니다. 그런 다음 P의 로컬 큐에 다시 넣어 다음 실행을 기다립니다.

Summary

이 글에서는 주로 Go 스케줄러 아키텍처 수준의 G-P-M 모델을 소개합니다. 이 모델을 통해 소수의 구현 방법을 설명합니다. 동시에 실행되는 많은 수의 고루틴을 지원하는 커널 스레드. 그리고 NetPoller, sysmon 등을 통해 Go 프로그램이 스레드 차단을 줄이고 기존 컴퓨팅 자원을 최대한 활용하도록 도와줌으로써 Go 프로그램의 운영 효율성을 극대화합니다.

더 많은 바둑 지식을 알고 싶다면 PHP 중국어 웹사이트 go 언어 튜토리얼 컬럼을 주목해주세요.

위 내용은 Go가 왜 그렇게 '빠른' 걸까요?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 腾讯技术工程에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제