운영 체제에서 각 프로세스에는 고유한 프로세스 ID가 있고 각 스레드에는 고유한 스레드 ID가 있습니다. 마찬가지로 Go 언어에서는 각 고루틴에 고유한 Go 루틴 ID가 있는데, 이는 패닉과 같은 시나리오에서 자주 발생합니다. 고루틴에는 고유 ID가 있지만 Go 언어는 의도적으로 이 ID를 얻기 위한 인터페이스를 제공하지 않습니다. 이번에는 Go 어셈블리 언어를 통해 고루틴 ID를 얻어보도록 하겠습니다.
공식 관련 자료에 따르면 Go 언어가 의도적으로 goid를 제공하지 않는 이유는 남용을 피하기 위해서입니다. 대부분의 사용자는 쉽게 goid를 얻은 후 후속 프로그래밍에서 goid에 크게 의존하는 코드를 무의식적으로 작성하기 때문입니다. goid에 대한 강한 의존성은 이 코드를 포팅하기 어렵게 만들고 동시 모델을 복잡하게 만듭니다. 동시에 Go 언어에는 수많은 고루틴이 있을 수 있지만, 각 고루틴이 언제 파괴되는지 실시간으로 모니터링하는 것은 쉽지 않으며, 이로 인해 goid에 의존하는 리소스가 자동으로 재활용되지 않게 됩니다( 수동 재활용 필요). 하지만 Go 어셈블리 언어 사용자라면 이런 걱정은 완전히 무시하셔도 됩니다.
참고: 고이드를 강제로 획득하면 '부끄러움'을 당할 수 있습니다. ?:
https://github.com/golang/go/blob/master/src/runtime/proc.go#L7120
이해를 돕기 위해 먼저 순수 Go에서 고이드를 구해 보겠습니다. 순수 Go에서 고이드를 얻는 성능은 상대적으로 낮지만 코드는 이식성이 좋고 다른 방법으로 얻은 고이드가 올바른지 테스트하고 검증하는 데에도 사용할 수 있습니다.
모든 Go 언어 사용자는 패닉 기능을 알아야 합니다. 패닉 함수를 호출하면 고루틴 예외가 발생합니다. 고루틴의 루트 기능에 도달하기 전에 복구 기능으로 패닉을 처리하지 않으면 런타임은 관련 예외와 스택 정보를 인쇄하고 고루틴을 종료합니다.
패닉을 통해 goid를 출력하는 간단한 예제를 구성해 보겠습니다.
package main func main() { panic("leapcell") }
실행 후 다음 정보가 출력됩니다.
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
Panic 출력 정보 goroutine 1 [running]의 1이 goid라고 추측할 수 있습니다. 하지만 프로그램에서 패닉 출력 정보를 어떻게 얻을 수 있습니까? 실제로 위 정보는 현재 함수 호출 스택 프레임에 대한 텍스트 설명일 뿐입니다. Runtime.Stack 함수는 이 정보를 얻는 기능을 제공합니다.
runtime.Stack 함수를 기반으로 현재 스택 프레임의 정보를 출력하여 goid를 출력하는 예제를 재구성해 보겠습니다.
package main func main() { panic("leapcell") }
실행 후 다음 정보가 출력됩니다.
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
따라서 런타임에서 얻은 문자열에서 goid 정보를 쉽게 구문 분석할 수 있습니다.Stack:
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
GetGoid 기능에 대한 자세한 내용은 설명하지 않겠습니다. Runtime.Stack 함수는 현재 고루틴의 스택 정보뿐만 아니라 모든 고루틴의 스택 정보(두 번째 매개변수로 제어)도 얻을 수 있다는 점에 유의하세요. 동시에 Go 언어의 net/http2.curGoroutineID 함수도 비슷한 방식으로 goid를 얻습니다.
공식 Go 어셈블리 언어 문서에 따르면, 실행 중인 각 고루틴 구조의 g 포인터는 현재 실행 중인 고루틴이 위치한 시스템 스레드의 로컬 저장소 TLS에 저장됩니다. 먼저 TLS 스레드 로컬 저장소를 얻은 다음 TLS에서 g 구조의 포인터를 얻은 다음 마지막으로 g 구조에서 goid를 추출할 수 있습니다.
다음은 런타임 패키지에 정의된 get_tls 매크로를 참조하여 g 포인터를 구하는 것입니다.
goroutine 1 [running]: main.main() /path/to/main.g
get_tls는 Runtime/go_tls.h 헤더 파일에 정의된 매크로 함수입니다.
AMD64 플랫폼의 경우 get_tls 매크로 함수는 다음과 같이 정의됩니다.
import ( "fmt" "strconv" "strings" "runtime" ) func GetGoid() int64 { var ( buf [64]byte n = runtime.Stack(buf[:], false) stk = strings.TrimPrefix(string(buf[:n]), "goroutine") ) idField := strings.Fields(stk)[0] id, err := strconv.Atoi(idField) if err!= nil { panic(fmt.Errorf("can not get goroutine id: %v", err)) } return int64(id) }
get_tls 매크로 함수를 확장한 후 g 포인터를 얻는 코드는 다음과 같습니다.
get_tls(CX) MOVQ g(CX), AX // Move g into AX.
실제로 TLS는 스레드 로컬 저장소의 주소와 유사하며, 해당 주소에 해당하는 메모리의 데이터가 g 포인터입니다. 좀 더 간단하게 설명할 수 있습니다.
#ifdef GOARCH_amd64 #define get_tls(r) MOVQ TLS, r #define g(r) 0(r)(TLS*1) #endif
위의 방법을 기반으로 getg 함수를 래핑하여 g 포인터를 얻을 수 있습니다.
MOVQ TLS, CX MOVQ 0(CX)(TLS*1), AX
그런 다음 Go 코드에서 g 구조의 goid 멤버의 오프셋을 통해 goid 값을 얻습니다.
MOVQ (TLS), AX
여기서 g_goid_offset은 goid 멤버의 오프셋입니다. g 구조는 Runtime/runtime2.go를 참조합니다.
Go1.10 버전에서는 goid의 오프셋이 152바이트입니다. 따라서 위 코드는 goid 오프셋도 152바이트인 Go 버전에서만 올바르게 실행될 수 있습니다. 위대한 톰슨의 신탁에 따르면, 열거와 무차별 대입은 모든 어려운 문제의 만병통치약입니다. Goid 오프셋을 테이블에 저장한 다음 Go 버전 번호에 따라 goid 오프셋을 쿼리할 수도 있습니다.
개선된 코드는 다음과 같습니다.
// func getg() unsafe.Pointer TEXT ·getg(SB), NOSPLIT, <pre class="brush:php;toolbar:false">const g_goid_offset = 152 // Go1.10 func GetGroutineId() int64 { g := getg() p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset)) return *p }-8 MOVQ (TLS), AX MOVQ AX, ret+0(FP) RET
이제 goid 오프셋이 마침내 출시된 Go 언어 버전에 자동으로 적응할 수 있게 되었습니다.
열거와 무차별 대입은 간단하지만 개발 중인 아직 출시되지 않은 Go 버전을 잘 지원하지 않습니다. 개발 중인 특정 버전에서는 고이드 멤버의 오프셋을 미리 알 수 없습니다.
런타임 패키지 내부에 있다면 unsafe.OffsetOf(g.goid)를 통해 멤버의 오프셋을 직접 얻을 수 있습니다. 또한 리플렉션을 통해 g 구조의 유형을 얻은 다음 해당 유형을 통해 특정 멤버의 오프셋을 쿼리할 수도 있습니다. g 구조는 내부 유형이므로 Go 코드는 외부 패키지에서 g 구조의 유형 정보를 얻을 수 없습니다. 하지만 Go 어셈블리 언어에서는 모든 기호를 볼 수 있으므로 이론적으로는 g 구조의 타입 정보도 얻을 수 있습니다.
모든 유형이 정의된 후 Go 언어는 해당 유형에 해당하는 유형 정보를 생성합니다. 예를 들어 g 구조는 g 구조의 값 유형 정보를 나타내는 유형·런타임·g 식별자와 포인터 유형 정보를 나타내는 유형·*런타임·g 식별자를 생성합니다. g 구조에 메소드가 있는 경우 go.itab.runtime.g 및 go.itab.*runtime.g 유형 정보도 생성되어 해당 유형 정보를 메소드로 표현합니다.
g 구조체의 타입을 나타내는 타입·런타임·g와 g 포인터를 얻을 수 있다면, g 객체의 인터페이스를 구성할 수 있습니다. 다음은 g 포인터 객체의 인터페이스를 반환하는 향상된 getg 함수입니다.
package main func main() { panic("leapcell") }
여기서 AX 레지스터는 g 포인터에 해당하고, BX 레지스터는 g 구조체의 유형에 해당합니다. 그런 다음 런타임·convT2E 함수를 사용하여 유형을 인터페이스로 변환합니다. g 구조의 포인터 유형을 사용하지 않기 때문에 반환된 인터페이스는 g 구조의 값 유형을 나타냅니다. 이론적으로는 g 포인터 형태의 인터페이스도 구성할 수 있지만 Go 어셈블리 언어의 한계로 인해 유형·*런타임·g 식별자를 사용할 수 없습니다.
g가 반환한 인터페이스를 기반으로 하면 goid를 쉽게 얻을 수 있습니다.
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
위 코드는 리플렉션을 통해 고이드를 직접 구합니다. 이론적으로는 반영된 인터페이스의 이름과 goid 멤버가 변경되지 않는 한 코드는 정상적으로 실행될 수 있습니다. 실제 테스트 후 위 코드는 Go1.8, Go1.9, Go1.10 버전에서 올바르게 실행될 수 있습니다. 낙관적으로, g 구조 유형의 이름이 변경되지 않고 Go 언어의 반영 메커니즘이 변경되지 않으면 향후 Go 언어 버전에서도 실행될 수 있어야 합니다.
성찰에는 어느 정도 유연성이 있지만 성찰의 성과는 항상 비판을 받아왔습니다. 개선된 아이디어는 리플렉션을 통해 고이드의 오프셋을 얻은 다음 g 포인터와 오프셋을 통해 고이드를 얻으므로 리플렉션은 초기화 단계에서 한 번만 실행하면 됩니다.
다음은 g_goid_offset 변수의 초기화 코드입니다.
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
올바른 goid 오프셋을 얻은 후 앞서 언급한 방식으로 goid를 얻습니다.
package main func main() { panic("leapcell") }
이 시점에서 goid를 얻기 위한 구현 아이디어는 충분히 완성되었지만 어셈블리 코드에는 여전히 심각한 보안 위험이 있습니다.
getg 함수는 NOSPLIT 플래그로 스택 분할을 금지하는 함수 유형으로 선언되어 있지만, getg 함수는 내부적으로 더 복잡한 런타임·convT2E 함수를 호출합니다. 런타임·convT2E 함수에 스택 공간이 부족하면 스택 분할 작업이 트리거될 수 있습니다. 스택이 분할되면 GC는 함수 매개변수, 반환 값 및 지역 변수의 스택 포인터를 이동합니다. 하지만 우리의 getg 함수는 지역 변수에 대한 포인터 정보를 제공하지 않습니다.
다음은 향상된 getg 기능의 전체 구현입니다.
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
여기서 NO_LOCAL_POINTERS는 함수에 로컬 포인터 변수가 없다는 의미입니다. 동시에 반환된 인터페이스는 0 값으로 초기화되며 초기화가 완료된 후 GO_RESULTS_INITIALIZED를 사용하여 GC에 알립니다. 이렇게 하면 스택이 분할될 때 GC가 반환 값과 지역 변수의 포인터를 올바르게 처리할 수 있습니다.
goid를 사용하면 고루틴 로컬 저장소를 구축하는 것이 매우 쉽습니다. goid 기능을 제공하기 위해 gls 패키지를 정의할 수 있습니다:
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
GLS 패키지 변수는 단순히 맵을 래핑하고 sync.Mutex 뮤텍스를 통해 동시 액세스를 지원합니다.
그런 다음 내부 getMap 함수를 정의하여 각 고루틴 바이트에 대한 맵을 얻습니다.
goroutine 1 [running]: main.main() /path/to/main.g
고루틴의 개인 지도를 얻은 후 추가, 삭제, 수정 작업을 위한 일반적인 인터페이스입니다.
import ( "fmt" "strconv" "strings" "runtime" ) func GetGoid() int64 { var ( buf [64]byte n = runtime.Stack(buf[:], false) stk = strings.TrimPrefix(string(buf[:n]), "goroutine") ) idField := strings.Fields(stk)[0] id, err := strconv.Atoi(idField) if err!= nil { panic(fmt.Errorf("can not get goroutine id: %v", err)) } return int64(id) }
마지막으로 고루틴에 해당하는 지도 리소스를 해제하는 Clean 기능을 제공합니다.
get_tls(CX) MOVQ g(CX), AX // Move g into AX.
이렇게 하면 미니멀한 고루틴 로컬 저장소 gls 객체가 완성됩니다.
다음은 로컬 저장소를 사용하는 간단한 예입니다.
#ifdef GOARCH_amd64 #define get_tls(r) MOVQ TLS, r #define g(r) 0(r)(TLS*1) #endif
고루틴 로컬 스토리지를 통해 다양한 수준의 기능이 스토리지 리소스를 공유할 수 있습니다. 동시에 리소스 누수를 방지하려면 고루틴의 루트 함수에서 defer 문을 통해 gls.Clean() 함수를 호출하여 리소스를 해제해야 합니다.
마지막으로 Go 서비스 배포에 가장 적합한 플랫폼을 추천해드리겠습니다: 도약셀
문서에서 더 자세히 알아보세요!
Leapcell 트위터: https://x.com/LeapcellHQ
위 내용은 고루틴 ID를 얻는 방법은 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!