[golang] 고루틴(Goroutine)

고루틴(Goroutine)

비동기 프로세스의 기본

고루틴은 여러 함수를 동시에 실행할 수 있는 논리적 가상스레드이다. 우리는 컴퓨터로 한 가지일만 하지 않는다. 사용자는 여러가지 프로그램을 실행하고 프로그램은 메모리(CPU)에 할당되어 처리된다. 이것을 바로 멀티 태스킹이라고 한다. CPU의 공간을 효율적으로 나눠 프로그램을 처리하는 것이다. 그런데 프로그램 안에서도 여러가지 일이 실행되고 처리된다. 이러한 프로세스(메모리에 할당된 프로그램)안의 실행 흐름을 스레드라고 한다. 당연히 하나의, 스레드를 사용한다면 주어진 일을 다소 바보같이 순서대로 하나씩 동기처리한다. 하지만 멀티 스레드가 되면 주어진 일을 동시에 처리하는 것이 가능한다. 쉽게 말해서, 멀티 프로세스는 동시에 여러 프고르ㅐㅁ을 실행하는 것이고 , 멀티 스레드는 프로그램 안에서 다양한 기능을 동시에 실행하는 것이다.

자바에서는 함수를 호출할 때 멀티 스레드를 사용한다. 하지만 go에서는 스레드보다 훨씬 가벼운 비동기 동시 처리를 구현해 각각의 일에 대해 스레드와 1대1로 대응하지 않고, 훨씬 적은 스레드를 사용한다. 메모리 측면에서 스레드가 1MB의 스택을 갖을때 고루틴은 훨씬 작은 KBㅇ 단위의 스택을 갖고 필요시에 동적으로 증가한다. 이는 굉장히 효율적이고 가벼운 기능으로서 비동기 프로세스를 구현할때 go언어 장점이 극대화된다. 또한 고채널을 이용해 고루틴간의 통신도 굉장히 용이하게 할수 있다.

즉, go언어에서는 함수에 고루틴을 선언함으로써 함수를 비동기적으로 동시에 실행할 수 있다. 이때 비동기적이란 한 번에 여러 일을 실행함을 말한다. 고루틴을 선언하는 방법은 함수 앞에 go 를 입력하면 된다. 고루틴을 사용한 함수는 다른 함수와 상관없이 동시에 실행된다.

package main

import "fmt"

func testGO(){
	fmt.Println("hello goorm!")
}

func main(){
	go testGO() // 고루틴으로 비동기 실행
}

이 함수를 실행하면 hello goorm이 실행되지 않고 프로그램이 종료된다. 이는 testGo() 함수를 고루틴으로 실행함으로써 main() 함수와 동시에 실행되기때문에 testGo() 함수의 hello goorm이 호출되기 전에 main이 종료되고 , 프로그램이 종료된다. 따라서 main 함수가 먼저 종료되지 않게 대기하기 위해서는 fmt.Scanln()를 입력해줘야 한다.

package main

import "fmt"

func testGO(){
	fmt.Println("hello goorm!")
}

func main(){
	go testGO() // 고루틴으로 비동기 실행
	
	fmt.Scanln() // 추가
}

이제 정말 고루틴으로 실행하는 함수가 동시에 비돌기적으로 실행되는지 피부로 느끼기 위해 반복문을 사용해 고루틴으로 함수를 호출해보겠습니다. 1부터 30까지 순차적으로 실행하는 반복문으로 난수를 생성해 함수 호출에 대기 시간을 설정하겠습니다. 이렇게 설정하면 비동기적으로 실행하기 때문에 숫자가 순서대로 출력되지 않을 것입니다.

난수를 생성하기 위해 "math/rand" 패키지, 시간 출력을 위해 "time"패키지를 import한다.

  • rand.Intn()는 정수형 난수를 생성하는 함수다 .
  • "time"에서 쓰이는 시간은 모두 duration형이다. 이는 "time"패키지 안에 선언된 구조체로써 int64형이다.
  • time.Slepp()은 프로그램에 대기 시간을 주는 함수이다. 괄호 안은 Duration 형을 써야한다.
package main

import (
	"fmt"
	"math/rand"
	"time"
)


type Duration int64
//int64를 Duration으로 type문으로 사용자 재정의함 


const (
	Nanosecond  Duration = 1
	Microsecond          = 1000 * Nanosecond
	Millisecond          = 1000 * Microsecond
	Second               = 1000 * Millisecond
	Minute               = 60 * Second
	Hour                 = 60 * Minute
)

func hello(n int) {
	r := rand.Intn(3) // 0부터 3까지 난수 생성
	time.Sleep(time.Duration(r) * time.Second)
	// 난수 r을 Duration형으로 형변환 후 second로 계산
	fmt.Println(n) // 뒤죽박죽으로 출력됨. 
}

func main() {
	for i := 0; i < 100; i++ {
		go hello(i)        // 고루틴 100개 생성 비동기 실행
	}

	fmt.Scanln()
}

코드를 실행해보면 고루틴에서 비동기적으로 호출되는 함수의 fmt.Println(n) 실행 시간을 랜덤으로 지정했기 때문에 출력되는 숫자들이 뒤죽박죽인 것을 확인할 수 있다. 차이를 느끼기 위해 고루틴을 위해 출력해보면 된다.

고루틴의 활용

이전 강의에서 main()함수 내에 고루틴으로 실행되는 함수들이 비동기적으로 호출될때 main() 함수가 먼저 종료되는 것을 막기 위해 fmt.Scanln() 함수를 사용해 고루틴이 모두 끝날때까지 대기하도록 했다. 하지만 이는 매우 주먹구구식 대처이다. 원래는 고루틴이 모두 끝날때까지 대기하는 기능이 따로 존재한다. 바로 syncWaitGroup이다.

WaitGroupsync 패키지에 선언되어있는 구조체로서 고루틴이 완료될때까지 대기한다. 그리고 이를 변수로 선언해 메소드를 활용할 수 있다.

  • Add() : 기다릴 고루틴의 수 설정
  • Done() : 고루틴이 실행된 함수 내에서 호출함으로써 함수 호출이 완료됐음을 알림
  • Wait() : 고루틴이 모두 끝날 때까지 차단
package main

import (
	"fmt"
	"math/rand"
	"time"
	"sync"
)

// wait은 포인터형 변수이기 때문에 *sync.WaitGroup으로 선언 
funs hello(n int, W *sync.WaitGroup){
	defer w.Done() //끝났음을 전달
	
	r := rand.Intn(3) // 3개의 난수 생성
	
	time.Sleep(time.Duration(r) * time.Second)
	
	fmt.Println(n)
}

func main(){
	wait := new(sync.WaitGroup) // waitgroup 생성 - new 키워드로 선언한 변수는 포인터형이다.  
	
	wait.Add(100) // 100개의 고루틴을 기다림. 기다릴 고루틴 개수 설정
	
	for i := 0; i<100; i++{
		go hello(i,wait) // wait을 매개변수로 전달
	}
	wait.Wait()// 고루틴이 모두 끝날때까지 대기 
	
}

위 코드를 보면 WaitGroup 생성 -> 고루틴 개수 설정 -> 끝났음을 전달 -> 모두 끝날 때까지 대기(비동기라 같이 진행됨) -> 종료 순서로 진행된다. 그리고 main()함수에서 new키워드를 사용해 WaitGroup의 wait 포인터 변수를 생성한다. 여기서 new 키워드로 선언한 변수는 포인터형이다. 따라서 매개변수로 사용할 때 &연산자를 사용하지 않아도 자동으로 주소를 참조한다.
그리고 wait.Add(100) 메소드를 호출해 고루틴 100개를 기다리도록 한다. 여기서 중요한 것은 wait은 포인터 변수이기 때문에 매개변수형을 *sync.WaitGroup형식으로 선언한 것이다. 만약에 *를 붙이지 않았다면 hello함수 내에 wmain() 함수의 wait이 다르게 인식되어 고루틴이 모두 종료되지않고 교착상태에 빠진다. 따라서 w.Done() 메소드를 호출하고 고루틴이 모두 종료됐으면 main()함수에서 호출되어 기다리고 있던 wait.Wait() 메소드도 대기를 멈추고 종료한다.

클로저에서의 고루틴

사실 WaitGroup은 클로저에서 많이 활용된다. 위 예시에서 보면 wait변수를 포인터 변수로 선언해 사용하면서 좀 복잡했다. 익명함수 클로저를 사용하면 클로저를 감싸고있는 힘수 내에 선언된 wait을 직접 접근할 수 있기 때문에 사용하기 편리하다.

package main
 
import (
    "fmt"
    "sync"
)
 
func main() {
    var wait sync.WaitGroup
    wait.Add(102)
 
	str := "goorm!"
	
    go func() {
        defer wait.Done()
        fmt.Println("Hello")
    }()
	
	go func() {
        defer wait.Done()
        fmt.Println(str)
    }()
 
	for i := 0; i<100; i++ {
		go func(n int) {
			defer wait.Done()
			
			fmt.Println(n)
		}(i)
	}
 
    wait.Wait()
}

다중 CPU 병렬 처리

고루틴을 사용하면 고루틴에서 실행되는 함수가 비동기 처리가 된다고 배웠다. 그리고 고루틴을 아무리 많이 만들어도 한 개의 CPU에서 시분할 처리한다. 그런데 요즘 디바이스들은 CPU거 복수개(듀얼 코어 이상)이기 때문에 go언어는 고루틴뿐만이 아니라 다중 CPU를 이용한 병렬 처리를 지원한다. 고루틴에서의 동시성은 독립적으로 실행되는 기능들이고, 다중 CPU의 병렬 처리는 계산들을 동시 실행하는 것이다. 동시성은 한 번에 많은 것들을 처리하고, 병렬 처리는 한 번에 많은 일을 하는 것에 관한 것이다.

예를 들어, K리그를 인터넷 스트리밍으로 시청한다고 생각해볼때 , 같은 시간에 10개 경기아 열려서 10개의 창을 모두 띄어놓고 모두 봤다. 이걸 동시성이라고 한다. 서로 독립된 각각의 기능들이 동시에 처리되는 것이다. 그런데 1개의 인터넷 스트리밍은 여러가지 일들을 실행한다. 예를들어 영상을 서버에서 가져오고, 사람들의 채팅 정보도 가져와야하는 등의 일을 한다. 이를 독립적인 CPU에서 각각의 일을 할당받아 수행하는 것을 병렬처리하고 하나.

그리고 go언어에서 다중CPU를 사용하는 것은 굉장히 간단하다. runtime 패키지에서 제공하는 함수를 이용하면 된다.

  • runtime.NumCPU(): 현재 디바이스의 CPU 개수를 반환
  • runtime.GOMAXPROCS() : 입력한 수만큼 CPU 사용, 입력한 수가 1 미만일 때 현재 설정 값을 반환하고 설정 값은 바꾸지 않음.
package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main(){
	runtime.GOMAXPROCS(runtime.NumCPU())
	//디바이스의 총 CPU 개수를 반환하고 그 값을 CPU 사용 값으로 
	
	fmt.Println(runtime.GOMAXPROCS(0))
	//현재 설정값 출력, 1미만이기 때문에 설정값 바꾸지 않음.
	
	var wait sync.Waitgroup
	wait.Add(100) // 기다릴 고루틴 개수 설정
	
	for i := 0; i <100; i++{
		go func(n int){
			defer wait.Done()
			fmt.Println(n)
		}(i)
	}
	
	wait.Wait()
}

좋은 웹페이지 즐겨찾기