개발

[golang] go channel, go routine

melonbbang-ruffy 2024. 11. 27. 15:52

channel

동시성과 병렬성에서 데이터 동기화는 매우 중요하다...! (동시성, 병렬성은 매우매우매우중요한 개념이므로 따로 다루겠습니다)
채널은 고루틴 사이 데이터 동기화를 위해 사용된다.
쉽게 말해 고루틴 간 데이터를 주고받는 통로 = 채널
채널은 make() 함수를 통해 생성된다.

make 함수에서 -> 
첫번째 인자: 채널로 전송하는 데이터 타입
두번째 인자: 버퍼 크기
아래는 예시)

done := make(chan string, 1) 
// chan 정의(channel) 그리고 채널에 보낼 데이터 타입 정의(string), 버퍼 크기는 1바이트


채널 <- 데이터 : 만들어진 채널에 데이터를 보냄(send)
<- 채널 : 채널에서 데이터를 받아옴(receive)

데이터를 주고 받을때 사용되는데, 상대편이 준비될 때까지 채널에서 대기함 -> lock을 별도로 걸지 않고 데이터를 동기화하는데 사용

// 정수형 채널 생성, 고루틴에서 해당 채널에 123이라는 정수 데이터를 보낸 후 다시 메인 루틴에서 채널로부터 123을 받는 코드
// 채널 생성 시 make() 함수 사용


// 굳이 sleep 사용 x, 채널로부터 데이터를 받을 때 상대편 고루틴에서 데이터를 전송할 때까지 계속 대기
package main

import "fmt"

func main() {
	ch := make(chan int) // 정수형 데이터를 보내는 채널을 생성

	go func() {
		ch <- 123 // 채널에 123이라는 정수형 데이터를 보냄
	}()

	var i int  // 정수형 변수 선언, 변수 선언 시에는 앞에 var 선언

	i = <- ch // 채널로부터 123이라는 정수형 데이터를 받음
	fmt.Println(i)
}

 

채널에서 데이터를 받을 때 : 채널 <- 데이터
채널에서 데이터를 보낼 때 : <- 채널


고 채널은 수신자/송신자가 서로를 기다리는 특징으로 인해 고 루틴이 끝날때까지 기다리게 하는 기능도 구현이 가능하다.
익명함수 + 고루틴에서 어떤 작업을 실행하면 메인 루틴(메인함수)은 done 채널에서 계속 수신을 기다리며 대기하게 된다.
익명함수에서 작업이 끝나고, done 채널에 true를 보내면 메인루틴은 이를 받고 프로그램 종료

package main

import "fmt"

func main() {
	done := make(chan bool)
	go func() { // 익명함수 실행
		for i := 0; i < 10; i++ {
			fmt.Println(i)
		}
		done <- true // done 채널에 true 데이터 전송
	}() // 여기서 익명 함수 호출!
	<-done // go 루틴이 끝날 때까지 대기
}

메인함수의 채널은 done 채널로부터 값을 받을 때까지(<- done) 기다리게 되고, 만약 값을 받을 경우 -> 프로그램을 종료하게 된다.

채널 버퍼링


unbuffered channel / buffered channel
unbuffered channel : 송신자가 수신자에게 데이터를 채널에 보내기 전까지 채널에 묶여있음 (다른 일을 못한다는 의미)
buffered channel : 수신자가 받을 준비가 안되어 있더라도? 지정된 버퍼만큼 데이터를 보내고 다른 일 수행 가능

버퍼 채널 -> make(chan type, N) 함수를 통해 생성 가능, 두번째 파라미터 N은 사용할 버퍼 바이트
make(chan int, 10) // 10바이트의 버퍼를 갖는 정수형 채널 생성

// 데드락 발생 코드
func main() {
	c := make(chan int)
	c <- 1 // 수신 루틴이 없으므로 데드락, 
	// 해당 채널을 받는 수신자 고루틴이 없으므로 데드락 발생
	fmt.Println(<-c) // 코멘트해도 데드락이 발생함니다...별도의 고루틴이 없기 때문

}



아래는 올바른 코드

func main() {
	ch := make(chan int, 1)
	ch <- 101 // 수신자가 없더라도 보낼 수 있음

	fmt.Println(<-ch)
}



채널 파라미터 타입?
생성된 채널 -> 함수에 인자로 전달 가능!
send / receive 타입 구분
send : chan <- string
receive : <- chan string

package main

func main() {
	ch := make(chan string, 1)
	sendChan(ch)
	receiveChan(ch)
}

func sendChan(ch chan<- string) {
	ch <- "Data" // Data라는 문자열을 채널로 보냄 / send
}

func receiveChan(ch <-chan string) {
	data := <-ch // 채널로부터 데이터를 받음 / receive
	println(data)
}



채널 종료
채널이 종료되어도 receive는 계속 실행 가능함
make로 만든 채널 -> 2개의 리턴값 가질 수 있음
(채널에서 발생된 메세지, 수신 성공 여부)

package main

func main() {
	c := make(chan int, 1)
	c <- 1
	close(c)

	if _, success := <-c; !success {
		println("data x") // 실행 x
	}
}



package main

import "fmt"

func main() {
	c := make(chan int, 1)
	c <- 1
	close(c)
	fmt.Println(<-c)

	if _, success := <-c; !success {
		println("더이상 데이타 없음.") // 실행됨
	}
}



채널 닫기(close)
close()함수를 이용해 채널 오픈 / 데이터 송신 후 채널을 닫을 수 있다.
채널을 닫을 경우 -> 해당 채널로 더이상 송신 x / 그러나 수신은 ㄱㄴ(채널로부터 데이터를 받는 건 가능)


for 문, if문을 이용해 계속 채널에 발생된 값을 수신할 수 있음
송신자가 데이터 송신 -> 채널 닫기 가능
수신자 -> 임의의 데이터를 채널이 닫힐 때까지 계속 수신 가능
방법1은 무한 for 루프 안에서 if 문으로 수신 채널의 두번째 파라미터를 체크하는 방식이고, 방법2는 방법1과 동일한 표현이지만, for range문으로 보다 간결하게 표현한 것이다. 채널 range문은 range 키워드 다음의 채널로부터 계속 수신하다가 채널이 닫힌 것을 감지하면 for 루프를 종료한다.

package main

func main() {
	ch := make(chan int, 2)

	ch <- 1
	ch <- 2

	close(ch)

	for {
		if i, success := <-ch; success {
			println(i)
		} else {
			break
		}
	}
} // close를 하지 않으면 deadlock 발생
// 왜 데드락이 발생하는가...?

 

 

package main

func main() {
	ch := make(chan int, 2)

	// 채널에 송신
	ch <- 1
	ch <- 2

	// 채널을 닫는다
	close(ch)

	// 방법1
	// 채널이 닫힌 것을 감지할 때까지 계속 수신
	/*
	   for {
	       if i, success := <-ch; success {
	           println(i)
	       } else {
	           break
	       }
	   }
	*/

	// 방법2
	// 위 표현과 동일한 채널 range 문
	for i := range ch {
		println(i)
	}
}


for 문으로 간략하게 표현한 코드는 아래와 같다.

package main

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	close(ch)

	for i := range ch {
		println(i)
	}
}



멀티채널 select
응용프로그램은 기본적으로 구조가 복잡하기 때문에 여러 채널을 활용한다.
이때, golang의 select를 활용해서 여러 채널들을 편리하게 관리할 수 있다.
select는 복수 채널들을 기다리면서 준비된(데이터를 보내온) 채널을 먼저 실행하는 기능을 제공한다.
● 여러개의 케이스에서 각각 다른 채널들을 기다리다가 준비가 된 케이스의 채널을 실행함
여러 케이스의 채널들이 준비가 안되어있다면 -> 계속 대기
가장 먼저 도착한 채널의 케이스 실행
복수 채널에 신호가 올 경우 -> 랜덤하게 하나 선택
그러나 default문이 있더라면 -> 준비가 안되더라도 대기하지 않고 바로 default 문 실행
아래는 for 루프 안에 select 문을 쓰면서 두개의 go routine이 모두 실행되기를 기다리고 있는 코드이다.
첫번째 run1()이 1초간 실행되고 done1 채널로부터 수신하여 해당 case를 실행하고, 다시 for 루프를 돈다. 
for루프를 다시 돌면서 다시 select문이 실행되는데 다음 run2()가 2초후에 실행되고 done2 채널로부터 수신하여 해당 case를 실행하게 된다. 
done2 채널 case문에 break EXIT 이 있는데, 이 문장으로 인해 for 루프를 빠져나와 EXIT 레이블로 이동하게 된다. Go의 "break 레이블" 문은 C/C# 등의 언어에서의 goto 문과 다른데, golang에서는 해당 레이블로 이동한 후 자신이 빠져나온 루프 다음 문장을 실행하게 된다. 따라서, 여기서는 for 루프 다음 즉 main() 함수의 끝에 다다르게 된다.

package main

import "time"

func main() {
	done1 := make(chan bool)
	done2 := make(chan bool)

	go run1(done1)
	go run2(done2)
EXIT:
	for {
		select {
		case <-done1:
			println("run1")
		case <-done2:
			println("run2")
			break EXIT
		}
	}
}

func run1(done chan bool) {
	time.Sleep(1 * time.Second)
	done <- true
}

func run2(done chan bool) {
	time.Sleep(2 * time.Second)
	done <- true
}

 

코드를 실행하면 1초 뒤 run1이 실행되고(time.Sleep(1 * time.Second)) 2초 뒤 run2가 실행되는걸 확인할 수있다.

done1에서 데이터를 받을 경우, run1이 출력되고, done2에서 데이터를 받을 경우, run2가 출력된다.

run1 함수 실행 -> sleep 후 인자로 받은 채널 (done1)에 true 라는 bool 데이터 보냄 -> case 실행

run 2 함수 실행 -> sleep 후 인자로 받은 채널 (done2)에 true 라는 bool 데이터 보냄 -> case 실행

 


동시성과 병렬성에 대해선 다음에 os 조금 복습해 보면서 다룰 예정....!

 

routine

go 런타임이 관리하는 논리적 / 가상적 스레드

go 키워드를 통해 루틴 생성 가능

런타임 때마다 새로운 go 루틴을 실행

go run("run1")
go run("run2")

 

go 루틴은 비동기적으로 함수의 루틴을 실행함 -> 여러 코드를 동시에 실행시키는 데 사용됨

사실 golang에서 동시에 실행되는 모든 활동들을 루틴이라고 칭해도 된다.

모든 프로그램은 기본적으로 하나의 메인 함수라는 go 루틴을 포함하며, go 루틴은 항상 백그라운드에서 작동한다.

★ 예제

package main

import (
	"fmt"
	"time"
)

func say(s string) {
	for i:=0; i<3; i++ {
		fmt.Println(s, "***", i)
	}
}

func main() {
	say("sync") // 함수 동기적 실행
	
	go say("async") // 비동기적 실행
	go say("async2") // 비동기적 실행2
	go say("async3")
	
	time.Sleep(time.Second * 3) // 3초 대기
}

해당 코드를 실행시켜보면

동기적 실행 -> 순차 실행

그러나 비동기적 실행할 경우 -> 순서가 뒤죽박죽이 된 것을 볼 수 있다.

이는 코드 순서와 상관없이 동시에 go로 정의된 루틴들이 모두 실행되며 출력되기 때문이다.

 

go 루틴 + 익명함수

go 키워드를 익명함수 앞에 선언하면 해당 익명함수가 비동기로 실행되게 된다.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wait sync.WaitGroup // WaitGroup 생성, 2개의 go 루틴 기다림
	wait.Add(2)
	
	go func() {
		// 고루틴을 사용한 익명함수
		defer wait.Done() // 함수가 끝날 때 Done() 호출
		fmt.Println("hello")
	}()
	
	go func(msg string) {
		// 익명함수에 파라미터 전달
		defer wait.Done() // 함수가 끝나기 직전 Done() 호출
		fmt.Println(msg)
	}("hi")
	
	wait.Wait() // 고루틴이 모두 끝날 때까지 대기
}

sync.WaitGroup -> 기본적으로 여러 고루틴이 끝날 때까지 기다림
Add() 메소드에 몇개의 고루틴을 기다릴 것인지 먼저 지정한후, 각 고루틴에 Done() 메소드를 호출, 그리고 메인 루틴에선 Wait() 메소드를 호출하여 고루틴이 모두 끝나기를 기다림

wait.Wait() -> Add에서 정의된 개수만큼 Done이 호출될 때까지 기다린다는 의미

 

func() {~} 앞에 go를 선언함으로서 go 루틴을 하나 생성한다.

해당 함수가 끝날 때 wait.Done() 함수를 호출할 수 있도록 defer 선언

 

 

익명함수

아래글 참조

더보기

함수명을 갖지않는 함수
함수 자체를 변수에 할당 or 다른 함수의 파라미터(인자)에 직접 정의되어 사용됨
익명함수가 변수에 할당되면 할당된 변수명은 익명함수의 함수명과 같이 취급됨
변수명(파라미터) 형식으로 해당 할당된 익명함수 호출 가능

 

예시)

func main() {
	sum := func(n ...int) int { // 익명함수 정의, ...int로 int형 배열 표현 가능
		s := 0
		for _, i := range n {
			s += i
		}
		return s
	} 
	result := sum(1, 2, 3, 4, 5)  // sum 변수를 통해 익명함수 호출
	println(result)
}

해당 코드를 실행시켜보면

func(n ...int) int 는 함수명을 선언하지 않은 익명함수로, 파라미터로 n이라는 정수형 배열을 받고, 리턴값으로 int형 값을 리턴하고 있다.

해당 익명함수를 sum 변수에 할당하고, sum 변수를 호출할 때 파라미터로 배열을 줌으로서 익명함수를 실행시키고 그에 대한 결과값을 리턴할 수 있다