티스토리 뷰

반응형

고루틴과 채널은 병행 처리를 위해서 사용되는 방법이다.

병행 처리
동시에 2개 이상의 작업이 실행되는 것

 

 

고루틴 (goroutine)

Go 프로그램 안에서 동시에 독립적으로 실행되는 흐름의 단위. 스레드와 비슷한 개념.

 

고루틴과 스레드의 차이점

  • 킬로바이트 정도의 아주 적은 리소스에서 동작
  • 적은 리소스에서 동작하여 한 프로세스에 수천, 수만 개의 고루틴을 동작시킬 수 있음
  • 정보를 공유하는 방식이 아닌 서로 메시지를 주고 받는 방식으로 동작
  • Lock으로 공유 메모리를 관리할 필요가 없음

 

고루틴을 생성하는 방법

go f(x, y)

고루틴에서 f(x, y)가 실행된다.

 

func main() {
	fmt.Println("main 함수 시작", time.Now())

	go long()
	go short()

	time.Sleep(5 * time.Second)
	fmt.Println("main 함수 종료", time.Now())
}

func long() {
	fmt.Println("long 함수 시작", time.Now())
	time.Sleep(3 * time.Second)
	fmt.Println("long 함수 종료", time.Now())
}

func short() {
	fmt.Println("short 함수 시작", time.Now())
	time.Sleep(1 * time.Second)
	fmt.Println("short 함수 종료", time.Now())
}

실행결과

  main 함수에서 long, short 함수는 go 키워드로 호출된다. 그리고 두 함수는 고루틴을 생성하여 실행된다. 코드 상에서는 long 함수가 먼저 호출이 되어있지만, 더 늦게 호출된 short 함수가 더 먼저 시작하는 것을 볼수 있다. 그리고 short 함수 long 함수 모두 0초 대에 시작되는 것을 볼수 있다.

 

고루틴 없이 함수를 실행했을 때

  고루틴 없이 단순히 함수를 실행했을 때 출력 결과이다. 고루틴을 사용했을 때는 거의 동시에 함수가 실행된 것을 볼 수 있다. 하지만 고루틴이 없을 때는 순차적으로 long 함수가 먼저 실행 되어 3초가 지난 뒤 short 함수가 실행되는 것을 알 수 있다.

 

고루틴을 사용할 때 주의점

  고루틴이 실행 중이더라도 메인 함수가 종료되면 프로그램이 종료된다. 프로그램이 비정상적으로 종료되는 것이다. 따라서 고루틴을 실행할 때 메인 함수가 종료되지 않게 하는 것이 중요하다. 메인 함수가 오래 실행되게 시간을 설정할 수 있지만, 고루틴의 실행 시간을 모르는 경우에는 시간 설정이 난감하다. 프로그램이 실행되는 시간보단 길어야 하지만 너무 오래 실행되면 효율적이지 못하다.

 

  위의 문제를 해결하기 위해서 Go에서는 채널을 사용한다. 채널을 통하여 고루틴의 종료 상황을 확인할 수 있다.

 

 

채널

  채널(channel)은 정보를 교환하고 실행의 흐름을 동기화하기 위해 사용한다.

동기화
가지고 있느 정보를 일치시키는 것

 

채널 생성

// 첫번째 방법
var ch chan string // 1. 채널 변수를 선언한다
ch = make(chan string) // 2. make 함수로 채널을 생성한다

// 두번째 방법
cone := make(chan bool) // make 함수로 채널 생성 후 바로 변수에 할당한다

  채널은 일반 변수와 같은 방식으로 선언하고, make 함수와 같이 생성한다. chan 키워드로 채널을 통해 주고 받을 데이터 타입을 명시해야 한다. 위의 코드는 채널을 생성하는 두 가지 방법이다.

 

  채널을 통해서는 지정한 타입의 데이터만 주고 받을 수 있다. 만약 타입에 상관없이 교환하고 싶다면 채널의 타입을 interface{}로 지정하면 된다.

 

데이터 교환

ch <- "msg" // ch 채널에 "msg" 전송
m := <- ch // ch 채널로부터 메시지 수신

  채널로 데이터를 교환할 때는 <- 연산자를 사용한다.  <- 을 기준으로 채널 변수가 왼쪽에 온다면 채널에 데이터를 전송하는 것이다. <- 오른쪽에 채널 변수가 온다면 채널로부터 데이터를 받아오는 것이다. 

 

  채널에 만약 데이터가 있을 때는 전송 가능한 상태가 될 때까지 전송하지 않고 대기한다. 그리고 데이터를 받을 때, 채널에 데이터가 없다면 데이터를 대기한다.

 

func main() {
	fmt.Println("main 함수 시작", time.Now())

	done := make(chan bool)
	go long(done)
	go short(done)

	<- done
	<- done
	fmt.Println("main 함수 종료", time.Now())
}

func long(done chan bool) {
	fmt.Println("long 함수 시작", time.Now())
	time.Sleep(3 * time.Second)
	fmt.Println("long 함수 종료", time.Now())
	done <- true
}

func short(done chan bool) {
	fmt.Println("short 함수 시작", time.Now())
	time.Sleep(1 * time.Second)
	fmt.Println("short 함수 종료", time.Now())
	done <- true
}

  그럼 이전의 코드를 위와 같이 바꿀 수 있다. 그럼 main의 시간을 늘려서 고루틴을 대기했다면, done이라는 채널을 통해서 long 함수와 short 함수가 끝났다는 메시지를 기다린다.

 

채널에서 주의할 점

교착 상태 & 경쟁 상태

  채널 사용시 교착 상태와 경쟁 상태가 발생할 수 있다. 데이터를 주고 받는 2개의 고루틴이 있는 상황에서 교착 상태에 빠질 수 있다.

교착 상태(deadlock)
두 개 이상의 작업이 서로의 작업이 끝나기를 기다리고 있어 작업이 완료되지 못하는 상태
경쟁 상태(race condition)
접근의 타이밍이나 순서 등이 결과값에 영향을 줄 수 있는 상태

 

https://go.dev/doc/articles/race_detector

 

Data Race Detector - The Go Programming Language

Data Race Detector Introduction Data races are among the most common and hardest to debug types of bugs in concurrent systems. A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write. See the

go.dev

-race로 실행했을 때

  Go는 교착상태와 경쟁 상태를 테스트하기 위해 Race Detector를 제공한다. -race 플래그와 함께 실행을 하면 경쟁 상태를 확인해 준다.

 

call-by-value

  채널은 call-by-value 방식으로 동작한다. 만약, 주소 값이 전달된다면 고루틴에서 동시에 값을 수정할 때 예상치 못한 결과가 발생할 수 있다. 따라서 포인터나 참조 값들은 참조 값에 동시에 접근할 수 없도록 뮤텍스로 제한하거나, 혹은 읽기 전용으로 인터페이스를 전달하면 안전하게 사용할 수 있다.

 

채널 방향

  채널은 기본적으로 양방향 통신이 가능하다. 필요한 경우 채널을 단방향으로도 설정할 수 있다.

chan<- string // 송신 전용 채널
<-chan string // 수신 전용 채널

  chan 키워드에 화살표를 왼쪽 혹은 오른쪽에 입력하여 방향을 설정할 수 있다. 만약 송신 전용 채널로부터 데이터를 수신할 시에는 컴파일 오류가 발생한다.

 

Buffered Channel

ch := make(chan int, 10)

  채널의 크기를 지정하여 해당 크기 만큼의 버퍼를 가질 수 있다. 버퍼의 크기를 make의 두 번째 인자도 입력하면 버퍼드 채널을 생성할 수 있다.

버퍼
데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리의 영역

 

  버퍼드 채널은 비동기 방식으로 동작한다. 채널과 마찬가지로 채널이 꽉 차면 데이터를 송신할 수 없고, 비어 있다면 수신할 수 없다.

 

실행 결과

  c는 크기가 2인 버퍼드 채널이다. c의 크기가 2이므로 데이터 3을 채널에 송신할 때 대기를 하게 된다. 따라서 main은 계속 자원을 기다리는 데드락 상태가 발생하게 된다. 따라서, 위의 코드를 실행하면 에러가 발생한다.

 

실행 결과

  3번째 데이터를 전송하는 것을 고루틴으로 실행시켰다. 고루틴은 데이터를 전송할 수 있을 때까지 대기를 한다. 이렇게 하면 코드가 데드락이 발생하지 않고 잘 실행되는 것을 확인할 수 있다.

 

close

  채널을 사용하지 않는 경우 채널을 종료시킬 수 있다. 이 경우 채널을 닫은 후에 메시지를 전송했을 때 에러가 발생한다.

 

  채널이 닫힌 상태인지 아닌지 2개의 매개변수를 받아 확인할 수 있다.

실행 결과

  v는 채널에서 가져온 값이고 ok는 채널이 닫혀있는지 아닌지 true/false로 알려주는 값이다. c가 close 되었기 때문에 else가 실행되는 것을 알 수 있다.

 

range

for i := range

  range를 사용하면 채널이 닫힐 때까지 채널로부터 수신을 시도한다.

 

  send는 채널을 통해서 데이터를 송신하고 채널을 종료하는 함수이다. main에서는 range 키워드를 통해서 채널이 끝나지 않는 동안 계속 데이터를 받아온다.

 

select

  select는 하나의 고루틴이 여러 채널과 통신할 때 사용한다. case와 함께 사용하여 실행 가능 상태가 된 채널이 있다면 case를 실행하는 방식으로 실행된다.

 

  c는 데이터를 송수신할 때 사용하는 채널이고 quit은 함수를 끝낼 때 사용하는 채널이다. 반복문의 실행이 끝나고 quit 채널을 통해서 데이터가 전송되었을 때, quit 채널을 통해서 데이터를 수신하는  case가 실행된다.

 

  위와 같이 default 케이스가 있다면, 모든 채널을 이용할 수 없을 때 default 케이스가 실행된다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함