Goroutine에 대해 알아보기
동시성은 웹 서버에서 동시에 여러 사용자 요청을 자율적으로 처리하는 작업 같은 독립된 활동의 컴퍼지션입니다. 동시성은 현재 많은 프로그램에서 제공됩니다. 웹 서버도 그중 한 가지 예이지만 상당한 양의 데이터를 일괄로 처리할 때도 동시성이 필요합니다.
Go에는 동시 실행 프로그램을 작성하는 두 가지 방식이 있습니다. 하나는 여러분이 스레드를 사용하여 다른 언어로 사용한 적이 있을지도 모르는 기존 방식입니다. 이 모듈에서는 프로세스를 전달하는 goroutine이라는 독립된 활동들 간에 값이 전달되는 Go의 방식에 대해 알아봅니다.
동시성에 대해 처음 배우는 경우라면 연습을 위해 작성할 코드의 각 부분을 검토하는 데 시간을 할애하는 것이 좋습니다.
Go의 동시성 접근 방식
일반적으로 동시 프로그램을 작성할 때 가장 큰 문제는 프로세스 간 데이터 공유입니다. Go는 채널을 통해 데이터를 주고받으므로 통신을 사용하는 다른 프로그래밍 언어와 접근 방식이 다릅니다. 이 접근 방식이 의미하는 것은 한 활동(Goroutine)만 데이터에 액세스할 수 있으며 기본적으로 경쟁 조건이 없다는 점입니다. 이 모듈에서 Goroutine과 채널에 대해 배우면 Go의 동시성 접근 방식에 대해 더 잘 이해할 수 있습니다.
Go의 접근 방식은 다음 슬로건으로 요약할 수 있습니다. "메모리를 공유하여 통신하지 마세요. 대신 통신하여 메모리를 공유합니다." 이 방법은 다음 섹션에서 다루겠지만 Go 블로그 게시물 통신을 통한 메모리 공유에서 자세히 알아볼 수도 있습니다.
앞서 언급했듯이 Go에는 하위 수준의 동시성 기본 형식도 포함되어 있습니다. 그러나 이 모듈에서는 Go의 동시성에 대한 관용적인 접근 방식만 다룹니다.
Goroutine을 먼저 살펴보겠습니다.
Goroutine
Goroutine은 운영 체제에 있는 기존 활동이 아닌 경량 스레드의 동시 활동입니다. 출력에 작성하는 프로그램과 두 숫자를 더하는 것과 같은 계산을 수행하는 또 다른 함수가 있다고 가정해봅니다. 동시 프로그램에는 두 함수를 동시에 호출하는 여러 Goroutine이 있을 수 있습니다.
프로그램이 실행하는 첫 번째 Goroutine이 main()
함수라고 말할 수 있습니다. 다른 Goroutine을 만들려면 함수를 호출하기 전에 다음과 같이 go
키워드를 사용해야 합니다.
func main(){
login()
go launch()
}
아니면 대부분의 프로그램은 다음 코드와 같이 익명 함수를 사용하여 Goroutine을 만드는 것을 선호한다는 것을 알게 될 것입니다.
func main(){
login()
go func() {
launch()
}()
}
Goroutine을 실제 작업에서 확인하기 위해 간단한 동시 프로그램을 작성해보겠습니다.
동시 실행 프로그래밍 작성
동시 실행 부분에만 집중해야 하므로 API 엔드포인트의 응답 여부를 확인하는 기존 프로그램을 사용해보겠습니다. 코드는 다음과 같습니다.
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
start := time.Now()
apis := []string{
"https://management.azure.com",
"https://dev.azure.com",
"https://api.github.com",
"https://outlook.office.com/",
"https://api.somewhereintheinternet.com/",
"https://graph.microsoft.com",
}
for _, api := range apis {
_, err := http.Get(api)
if err != nil {
fmt.Printf("ERROR: %s is down!\n", api)
continue
}
fmt.Printf("SUCCESS: %s is up and running!\n", api)
}
elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}
이전 코드를 실행하면 다음과 같은 출력이 표시됩니다.
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 1.658436834 seconds!
여기에 특별한 것은 없지만 효율성을 더 높일 수 있습니다. 모든 사이트를 동시에 확인하는 건 어떨까요? 이 프로그램은 2초에 가까운 시간이 아닌 500ms 이내에 완료할 수 있습니다.
동시에 실행해야 하는 코드 부분을 살펴보면 사이트로의 HTTP 호출입니다. 즉, 프로그램에서 확인 중인 각 API에 대해 Goroutine를 만들어야 합니다.
Goroutine를 만들려면 함수를 호출하기 전에 go
키워드를 사용해야 합니다. 하지만 여기에는 함수가 없습니다. 코드를 리팩터링하여 다음과 같이 새 함수를 만들어 보겠습니다.
func checkAPI(api string) {
_, err := http.Get(api)
if err != nil {
fmt.Printf("ERROR: %s is down!\n", api)
return
}
fmt.Printf("SUCCESS: %s is up and running!\n", api)
}
for
루프가 아니므로 continue
키워드는 더 이상 필요하지 않습니다. 함수의 실행 흐름을 중지하려면 return
키워드를 사용하면 됩니다. 이제 다음과 같이 main()
함수의 코드를 수정하여 API당 Goroutine 하나를 만들어야 합니다.
for _, api := range apis {
go checkAPI(api)
}
프로그램을 다시 실행하고 어떻게 되는지 확인합니다.
프로그램에서 API를 더 이상 확인하지 않는 것 같습니다. 다음 출력과 같은 정보가 표시될 것입니다.
Done! It took 1.506e-05 seconds!
빠르게 진행됐네요! 무슨 일이 일어났나요? Go가 루프 내에서 각 사이트마다 Goroutine을 생성하고 다음 줄로 바로 이동했기 때문에 프로그램이 완료되었다는 최종 메시지가 표시되고 즉시 다음 줄로 이동했습니다.
checkAPI
함수가 실행되는 것처럼 보이지 않더라도 실행 중입니다. 완료할 시간이 없었을 뿐입니다. 루프 바로 뒤에 다음과 같이 슬립 타이머가 포함되면 어떻게 되는지 보세요.
for _, api := range apis {
go checkAPI(api)
}
time.Sleep(3 * time.Second)
이제 프로그램을 다시 실행하면 다음과 같은 출력이 표시될 수 있습니다.
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 3.002114573 seconds!
제대로 작동하고 있는 것 같죠? 정확하지는 않습니다. 새 사이트를 목록에 추가하려면 어떻게 해야 하나요? 아마도 3초로는 부족할 것입니다. 어떻게 알까요? 관리할 수 없습니다. 더 나은 방법이 있어야 합니다. 채널에 대해 이야기할 때 다음 섹션에서 이에 대해 다루겠습니다.