채널-1

채널-1

채널에 대해 알아보자.


고루틴의 데이터 통로 : 채널


고루틴을 사용할 때 우리가 이전에는 생각해보지 못한 문제가 발생할 수 있습니다. 우리가 알고있는 함수의 기본 형식 예시를 살펴보고 어떤 문제가 발생할 수 있을지 생각해봅니다.

package main

import "fmt"

func main() {
	var a, b = 10, 5
	var result int
	
	func() {
		result = a + b
	}()
	
	fmt.Printf("두 수의 합은 %d입니다.", result)
}

사진 동기적인 함수들의 실행 흐름은 전혀 문제될 것이 없습니다. 그런데 만약 익명 함수고루틴에서 호출하면 어떻게 될까요? 익명함수에서 연산한 결과값이 printf() 함수에 전달되기도 전에 프로그램이 종료될 것입니다.
고루틴비동기적으로 실행되기 때문에 다른 고루틴에서 실행되는 함수의 종료 여부와는 상관없이 진행됩니다. 만약 값을 연산하고 반환하는 고루틴(main)이 먼저 종료된다면 문제가 발생하게 됩니다. 그래서 고루틴끼리 서로 값을 주고받는 통로가 필요합니다. 그것이 바로 채널(Channel)입니다. 이는 고루틴을 사용하는데 있어 굉장히 중요한 역할을 합니다.

package main

import "fmt"

func main() {
	var a, b = 10, 5
	var result int
	
	go func() {
		result = a + b
	}()
	
	fmt.Printf("두 수의 합은 %d입니다.", result)
}

사진 모든 고루틴이 종료되기 전까지 대기하는 방법으로

1. 주먹구구식으로 main() 함수 마지막에 fmt.Scanln() 함수를 쓰는 방법
2. WaitGroup 사용으로 모든 고루틴 종료 대기

그런데 말 그대로 이는 고루틴이 모두 종료되는 것을 기다리게 하는 용법일 뿐이지 고루틴 사이에 흐름을 제어하지는 않습니다. 채널고루틴 사이에서 값을 주고받는 통로 역할을 하고, 송/수신자가 서로를 기다리는 속성때문에 고루틴의 흐름을 제어합니다. 그리고 채널의 데이터를 주고 받을때까지 해당 고루틴을 종료하지 않아 별도의 lock을 하지 않고도 데이터를 동기화 하는데 사용합니다.

채널은

"make(chan 데이터타입)" 형식으로 생성합니다.
채널의 데이터 송/수신은 '<-' 연산자를 이용합니다.
체널에 값을 보낼 때는 채널 <- 데이터, 채널에서 값을 받을 때는 <- 채널 입니다. 값을 받을 때는 :=이나 =을 이용해 변수에 바로 값을 대입할 수 있습니다.
채널에서 값을 받을 때까지만 대기합니다. 가져오면 바로 다음 코드를 실행합니다.
package main

import "fmt"

func main() {
	var a, b = 10, 5
	var result int
	
	c := make(chan int)
	
	go func() {
		c <- a + b
	}()
	
	result = <-c
	fmt.Printf("두 수의 합은 %d입니다.", result)
}

사진 채널의 값을 수신할 때 꼭 변수에 대입하지 않아도 됩니다. 단지 <- 채널 형식으로 입력하면 수신을 받을 때까지 대기하고 별도의 값을 받지는 않습니다. 여기서 주의할 점은 위에서 고루틴B의 함수 종료 시점입니다. 고루틴B는 고루틴A로 송신한 데이터가 수신이 될때까지 대기합니다. 그런데 그 이후에 main() 함수(고루틴A)가 종료되어 프로그램이 종료되면 고루틴B는 끝까지 실행되지 않고, 종료되지 않은 채 프로그램이 종료될 수 있습니다.(채널의 역할이 끝났다면 고루틴의 본질은 비동기이기때문에) 따라서 데이터 송/수신 시점이 함수가 대기하는 시점과 관련있기 때문에 주의해야합니다.

package main

import "fmt"

func main() {
	var str = "Hello Goorm!"
	done := make(chan bool)
	
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(str, i)	
		}
		
		done <- true //채널에 true를 송신함
	}()

	<- done //수신함으로써 대기를 끝냄
}

비동기 채널과 버퍼


채널은 고루틴간의 데이터 송/수신을 위해 존재하는 것이기 때문에 채널을 사용하기 위해서는 고루틴을 꼭 사용해야한다고 했습니다. 따라서 채널을 이용해 비동기 프로세스에서 데이터를 원활하게 주고받을 수 있으며, 고루틴간의 흐름을 제어할 수 있는 것입니다. 이렇게 채널은 비동기 프로세스의 기능을 원활하게 해주고 극대화시킵니다.

채널을 사용할 때 간과할 수 있는 치명적인 오류가 발생할 수 있는데 바로 ‘데드락(Deadlock)’입니다. 데드락은 “둘 이상의 프로세스(함수)가 서로 가진 한정된 자원을 요청하는 경우 발생하는 것으로, 프로세스가 전진되지 못하고 모든 프로세스가 대기 상태가 되는 것”을 말합니다. 채널을 사용할 때는 main() 함수에서 고루틴이 무한 대기 상태가 됐을 때 데드락이 발생합니다. 쉽게 말해서, main() 함수에서 송/수신 채널이 대기 상태가 되면 프로그램이 진행되지 않아 종료되지 않는 것입니다.

package main
 
import "fmt"
 
func main() {
	c := make(chan string)
	
	c <- "Hello goorm!"
	
	fmt.Println(<-c)
}

채널을 생성하고 데이터를 보내고있는데 데이터를 받는 수신자(수신 루틴)가 없기 때문에 값을 수신할 때까지 무한 대기하는 데드락이 발생하는 것입니다.

비동기 채널 버퍼


채널에서 송/수신이 꼭 일대일 대응을 해야하기때문에 좀 번거로운 상황이 생길 수 있습니다. 그래서 이를 중재하는 역할을 하는 ‘버퍼’라는 것이 있습니다. 송신 루틴에서 수신 루틴으로 데이터를 바로 전달하는 것이 아니라 특정 개수의 버퍼를 만들어 송신자는 버퍼로 데이터를 보내고, 수신자는 버퍼에서 데이터를 가져오게끔 합니다. 쉽게 말해서, 송/수신자를 연결하는 통로 중간에 데이터를 잠깐 저장할 수 있는 공간을 마련하는 것입니다. 버퍼를 만드는 형식은 "make(chan 데이터타입, 버퍼 개수)" 입니다.

사진 송신 루틴은 수신자가 없어도 버퍼에 보내면 일을 끝내고, 수신 루틴은 일단 값을 받으면 송신 루틴의 일이 끝나든 아니든 자신의 일을 끝냅니다.

package main
 
import "fmt"
 
func main() {
	c := make(chan string, 1)

	c <- "Hello goorm!"
	
	fmt.Println(<-c)
}

비동기 채널 버퍼에서 고루틴의 대기 조건을 정리해보겠습니다.

송신 루틴은 버퍼가 가득차면 대기합니다.(보내고 할 일을 함. 보낸 순간 버퍼가 가득찼으면 대기, 버퍼에 빈 공간이 생기면 하던 일 마저 끝냄.)
수신 루틴은 버퍼에 값이 없으면 대기합니다.(버퍼에 값이 들어올 때까지)
package main

import (
	"fmt"
)

func main() {
	done := make(chan bool, 2)

	go func() {
		for i := 0; i < 6; i++ {
			done <- true

			fmt.Println("고루틴 : ", i)
		}
	}()

	for i := 0; i < 6; i++ {
		<-done                    
		
		fmt.Println("메인 함수 : ", i)
	}	
}

송신자는 수신자가 직접 데이터를 받을때까지 대기하지 않고 버퍼에 값을 보내기만 하면 다음 코드를 실행하기 때문에 훨씬 효율이 높아집니다. 물론 버퍼가 가득 차서 더이상 송신할 수 없을 때는 다음 코드를 실행하지 않고 채널에 묶여버립니다. 또한 main() 함수의 수신 루틴은 한개 받고 한개를 처리할 필요 없이 버퍼에 값이 있으면 바로바로 꺼내 씁니다. 똑같이 더이상 버퍼에 값이 송신되지 않으면 수신 루틴은 무한 대기 상태가 됩니다.

동기 채널


동기 채널은 이름만 보아도 알 수 있듯이 비동기 채널과 상반되는 개념입니다. 원래는 동기 채널이 채널의 기본 형태지만, 동기 채널은 단순히 송/수신 채널이 여러개여서 송신 루틴과 수신 루틴이 번갈아가면서 실행되는 것을 말합니다.

package main

import (
	"fmt"
	"time"
)

func main() {
	done := make(chan bool)

	go func() {
		for i := 0; i < 4; i++ {
			done <- true

			fmt.Println("고루틴 : ", i)
		}
	}()

	for i := 0; i < 4; i++ {
		<-done
		
		fmt.Println("메인 함수 : ", i)
		
		time.Sleep(time.Second)
	}	
}

단순히 채널로 데이터를 송신하고 수신함으로써 루틴을 왔다갔다 하며 실행하는 것을 보여줍니다. 그 모습이 눈에 보이게 하기위해 수신 루틴인 main() 함수 마지막에 time.Sleep(time.Second)을 입력했습니다. 사진


© 2022. All rights reserved. 신동민의 블로그