본문 바로가기

Coding/go

[golang] 채널 (Channel)

고루틴

고루틴이 비동기여서, 메인이 먼져 종료가 되여 문제가 있습니다. 아래는 문제의 코드입니다.

WaitGroup을 통해 대기하면 되겠지만, 그건 기다리게 할뿐 고루틴 사이에 흐름을 제어하지는 않습니다.

 

package main
import "fmt"

func main() {
    var a, b = 10, 5
    var result int

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

채널

일단 고루틴이 접근할 수 있는 고루틴만의 공간을 채널이라고 생각해봤습니다.

채널은 값을 받을 때까지 대기하고 (main)

채널이 값을 받을 때 까지 고루틴도 대기 합니다.

 

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)
    fmt.Printf("두 수의 합은 %d입니다.", <-c)
}

 

버퍼가 없는 채널

동기 채널인데 헷갈려서 '버퍼가 없는 채널'이라고 했습니다.

역시 값을 받을 때까지 대기하고

메인함수에서 받아야 고루틴도 대기하다가 끝납니다.

송신 루틴과 수신 루틴이 번갈아가며 실행합니다. 보내고 기다리고 저쪽이 받았으면 이쪽도 끝내고.. 세트로 움직입니다.

 

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)
    }    
}

 

결과를 보면 프린트문 때문에 밀리긴하지만 고루틴->메인함수 가 세트로 움직입니다.

고루틴 :0

메인 함수: 0

메인 함수: 1

고루틴: 1

메인함수: 2

고루틴: 2

메인 함수: 3

고루틴: 3

채널과 버퍼 (비동기채널)

교착상태가 있을수 있다고 합니다.(DeadLock)

고루틴에서 송신해야하는데, 송신코드는 있는데 송신하는 고루틴이 없어서, 수신자(main)가 계속기다리는 상황이 나오니 송신하는 부분에서 DeadLock 코드라고 알려주고 에러를 출력합니다.

 

package main

import "fmt"

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

 

고루틴(송신자)와 고루틴(main,수신자)가 1대1 대응이여야 데드락이 안걸리는데,(중요! 그래서 고루틴안에서 만 송신을 한것 입니다.) 이게 번거롭다고 합니다.

중간에 버퍼를 두면 괜찮아 진다고 합니다.

 

package main

import "fmt"

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

버퍼를 하나 만들고 1:1 대응 할 필요없으니, 고루틴 하나(main)에서 채널생성하고, 버퍼로 보내고, 버퍼에서 받습니다.

기본 룰을 보고 아래 코드를 확인해보겠습니다.

  • 송신 고루틴은 버퍼가 가득차면 대기합니다.
  • 수신 고루틴은 버퍼가 비어있으면 대기합니다.
package main

import (
    "fmt"
)

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

    go func() {
        for i := 0; i < 6; i++ {
            done <- true
            fmt.Println("고루틴 : ", i, "버퍼사이즈:", len(done))
        }
    }()

    for i := 0; i < 6; i++ { // 만약 7이면 하나가 무한 대기 -> 데드락 ㅜ
        <-done
        fmt.Println("메인 함수 : ", i, "버퍼사이즈:", len(done))
    }
}

 

결과는 10의 9은 아래와 같이 나오지만, 다르게 나올수도 있습니다. 채널의 버퍼 2개를 나두고 치열하게 경쟁하며 작동하는 것을 알 수 있습니다. 결과가 그때그때 다르지만 어떤과정을 거쳤는지 분석을 해봤습니다.

  1. 고루틴 : 0 버퍼사이즈: 0 // 고루틴이 들어왔는데 버퍼가 0인 이유는 버퍼에 하나 들어오고 0이 출력하려는데 그사이에 메인함수가 버퍼를 하나 빼고 0을 출력하려하다 멈추고 고루틴이 실행됩니다. 그리고 고루틴에서 출력이 되면 0이 출력되고 버퍼사이즈가 0 인상태가 됩니다.
  2. 고루틴 : 1 버퍼사이즈: 1 // 버퍼가 [True] 상태입니다.
  3. 고루틴 : 2 버퍼사이즈: 2 // 버퍼가 [True, True] 상태입니다.
  4. 메인 함수 : 0 버퍼사이즈: 0 // 버퍼가 꽉차 있으면 고루틴은 대기, 메인함수는 까 출력못한 0을 출력하고, 그때 당시 버퍼사이즈인 0을 출력합니다.
  5. 메인 함수 : 1 버퍼사이즈: 2 // 버퍼가 실제로는 현재 [True, True] 상태니깐 하나를 빼서[True]상태로만들고 1을 출력하려고 하는데 멈추고, 고루틴이 비어있는 순간 치고 들어와서 [True, True]가 됩니다. 3은 걸린상태가 됩니다.그리고 꽉 차니 다시 메인함수에서 걸렸던 1이 출력됩니다.
  6. 메인 함수 : 2 버퍼사이즈: 1 // [True] 가 되고 2가 출력됩니다.
  7. 메인 함수 : 3 버퍼사이즈: 0 // [] 가 되고 3이 출력됩니다.
  8. 고루틴 : 3 버퍼사이즈: 2 // 파랑색 부분이 실행됩니다. 실제 버퍼는 비어 있습니다. 비어있으면 메인함수는 접근못합니다.
  9. 고루틴 : 4 버퍼사이즈: 0 // 버퍼에 하나 추가하고 [True] 4가 출력하려는데. 메인함수에서 True를 가져갑니다 그리고 메인에서 4가 걸립니다. 그럼 4가 걸리게 되고 4가 출력하고 버퍼는 [] 비어버립니다
  10. 고루틴 : 5 버퍼사이즈: 1 // [True,] 하나를 추가하고 5가 출력됩니다.
  11. 메인 함수 : 4 버퍼사이즈: 0 // 보라색 부분이니깐 빈 버퍼와 4가 찍히게 됩니다.
  12. 메인 함수 : 5 버퍼사이즈: 0 // 실제 버퍼 상태는 [True]이니 하나를 빼고 5가 출력됩니다.

하나 넣고 하나 빼고 일대일 대응했던것 보다 큐를 하나 두니 유연해집니다.

비동기 상황에서 채널은 뭔가 복잡해서 그냥 버퍼 2개를 두고 치열하게 경쟁하며 실행되는걸로 이해하고 넘어가면 될것 같습니다.

채널닫기

만약 수신하는 곳이 명확하지 않은 채 채널로 데이터를 송신한다면 채널에 묶여 무한 대기 상황이 발생합니다.

만약에 송신이 2개고, 수신이 3개면 아래처럼 하나가 데드락(무한대기)에 걸리게 됩니다.

<- c 이부분은 리턴값을 두개로 받을수도 있습니다. v, open = <- c 이런식으로 boolean 값도 리턴받을 수 있습니다.

 

package main
 
import "fmt"

func main() {
	c := make(chan string, 2) // 버퍼 2개 생성
	
	// 채널(버퍼)에 송신
	c <- "Hello"
	c <- "goorm"
	
        // close(c) // 이거 살리면 무한 대기 상황 발생 x
    
	// 채널 수신
	fmt.Println(<-c)
	fmt.Println(<-c)
	fmt.Println(<-c) // 무한 대기 상황 발생
}

 

채널수신 부분 순회

for range 를 사용하면 수신자이면서 닫힌것만 출력이 됩니다. 제대로 닫혀야 true가 리턴되기 때문입니다.

 

package main
 
import "fmt"

func main() {
	c := make(chan int, 10)

	for i := 0; i<10; i++ {
		c <- i
	}
	close(c)
	
	for val := range c { // <- c를 사용하지 않음
		fmt.Println(val)
	}
}

송신전용, 수신전용 채널

지금까지 송신하는 고루틴에서는 송신만, 채널에서 데이터를 수신하는 고루틴은 수신만 했습니다.

하지만  채널은 송신과 수신에 있어 자유롭습니다. 

보내고 받고(1), 보내고 받고(2),  channel2에서 보내고 받고(3) 보내고 받고(4) 순차적으로 이뤄집니다.

버퍼가 없으니, 다시 고루틴(송신자)와 고루틴(main,수신자)가 1대1 대응이여야 데드락이 안걸리는 상황입니다.

 

package main
 
import "fmt"

func main() {
	c := make(chan int)
	
	go channel1(c)
	go channel2(c)

	fmt.Scanln()
}

func channel1(ch chan int) {
	ch <- 1
	ch <- 2
	
	fmt.Println(<-ch)
	fmt.Println(<-ch)
	
	fmt.Println("done1")
}

func channel2(ch chan int) {
	fmt.Println(<-ch)
	fmt.Println(<-ch)
	
	ch <- 3
	ch <- 4
	
	fmt.Println("done2")
}

 

전용 채널은 아래처럼 사용됩니다. 특별한건 없습니다.  대신 매개변수 선언 부분이 다릅니다. (버퍼가 없으니 동기)

 

package main
 
import "fmt"

func main() {
	c := make(chan int)
	
	go sendChannel(c)
	go receiveChannel(c)

	fmt.Scanln()
}

func sendChannel(ch chan<- int) {
	ch <- 1
	ch <- 2
	// <-ch 오류 발생
	fmt.Println("done1")
}

func receiveChannel(ch <-chan int) {
	fmt.Println(<-ch)
	fmt.Println(<-ch)
	//ch <- 3 오류 발생
	fmt.Println("done2")
}

 

수신전용 채널을 리턴하려면 아래 코드 처럼 합니다. 리턴값만 확인해주면 될 것 같습니다. (버퍼가 없으니 동기)

 

package main

import "fmt"

func main() {
	ch := sum(10, 5)
	
	fmt.Println(<-ch)
}

func sum(num1, num2 int) <-chan int {  // 수신전용 채널은 리턴값을 이렇게합니다.
	result := make(chan int)
	
	go func() { // 버퍼가 없는 채널에선 송신 고루틴과 수신 고루틴이 1대1 대응이여야함
		result <- num1 + num2 // go  키워드가 있어서 송신 고루틴이 생긴거
        // close(result) // 여기서 채널을 닫아주는게 좋을것 같습니다.
	}()
	
	return result
}

 

이쯤오면 앞에 내용을 까먹어서 아래처럼 해도 될 것 같지만 그렇지 않습니다. 왜냐하면 일단 1대1 대응을 하기위해 고 루틴을 만들고 채널로 송신해야 합니다. 그리고 나머지 수신전용 채널을 리턴했지만 고루틴에서 리턴이 안됩니다. 저런 문법은 없습니다.  함수안에서 고루틴을 만들고 채널을 통해 데이터를 넘기는게 최선입니다.

 

// 잘못된 코드

func main() {
	var ch chan int
	ch = go sum(10, 5) // 잘못된 문법

	fmt.Println(<-ch)
}

func sum(num1, num2 int) <-chan int {
	result := make(chan int)
	result <- num1 + num2
	return result
}

 

송신전용 채널과 수신전용 채널을 이용한 코드입니다. (버퍼가 없으니 동기)

버퍼가 없으니 속도나 이런건 장점이 없어도 어떻게 제어하는지 간단히 훑어볼 수 있었습니다.

비동기(고루틴)의 흐름 제어 정도로 이해 하면 될 것 같습니다.

버퍼가 있으면 비동기(고루틴)들이 더 치열하게 작동합니다.

 

package main

import "fmt"

func main() {
	numsch := num(10, 5)
	result := sum(numsch)
	//채널 result는 수신만 할 수 있음
	fmt.Println(<-result)
}

func num(num1, num2 int) <-chan int {
	numch := make(chan int)

	go func() {
		numch <- num1
		numch <- num2 //송신 후
		close(numch)
	}()

	return numch //수신 전용 채널로 반환
}

func sum(c <-chan int) <-chan int {
	//채널 c는 수신만 할 수 있음
	sumch := make(chan int)

	go func() {
		r := 0
		for i := range c { //채널 c로부터 수신
			r = r + i
		}
		sumch <- r // 송신 후
	}()

	return sumch //수신 전용 채널로 반환
}

채널 select

아래는 문제가 있는 코드입니다.

ch2가 먼저 수신되어도 항상 ch1을 기다렸다가 ch1 이후에 ch2가 찍히는 비효율적인 현상입니다. 

 

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan bool)
	ch2 := make(chan bool)

	go func() {
		for {
			time.Sleep(1000 * time.Millisecond)
			ch1 <- true
		}
	}()

	go func() {
		for {
			time.Sleep(500 * time.Millisecond)
			ch2 <- true	
		}
	}()

	go func() {
		for {
			<-ch1
			fmt.Println("ch1 수신")
			<-ch2
			fmt.Println("ch2 수신")
		}
	}()

	time.Sleep(5 * time.Second)
}

 

그래서 아래 코드처럼 switch-case 와 유사한 select-case 을 사용합니다.

 

	go func() {
		for {
			select {
			case <-ch1:
				fmt.Println("ch1 수신")
			case <-ch2:
				fmt.Println("ch2 수신")
			}
			
		}
	}()

 

case 옆에는 표현식이 가능해서 아래 완성된 코드 처럼 할당, 송신이 가능합니다.

 

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	ch3 := make(chan string)
	
	go func() {
		for {
			time.Sleep(200 * time.Millisecond)
			c := <- ch3
			fmt.Printf("ch3 데이터 %s 수신\n", c)
		}
	}()

	go func() {
		for {
			time.Sleep(1000 * time.Millisecond)
			ch1 <- 10
		}
	}()

	go func() {
		for {
			time.Sleep(500 * time.Millisecond)
			ch2 <- 20
		}
	}()

	go func() {
		for {
			select {
				case a := <-ch1:
				fmt.Printf("ch1 데이터 %d 수신\n", a)
				case b := <-ch2:				
				fmt.Printf("ch2 데이터 %d 수신\n", b)
				case ch3 <- "goorm":
				}
		}
	}()
	
	time.Sleep(5 * time.Second)
}

 

실습

1.  select-case 밖에 루프를 탈출하는 방법:

qastack.kr/programming/11104085/in-go-does-a-break-statement-break-from-a-switch-select

2. 버퍼를 사용하지 않는 채널은 한개의 데이터만 송/수신할 수 있습니다. 따라서 송/수신이 여러번 반복되는 루틴에서는 효율적이지 못합니다:

edu.goorm.io/learn/lecture/2010/%ED%95%9C-%EB%88%88%EC%97%90-%EB%81%9D%EB%82%B4%EB%8A%94-%EA%B3%A0%EB%9E%AD-%EA%B8%B0%EC%B4%88/lesson/389824/%EB%8F%99%EA%B8%B0-%EC%B1%84%EB%84%90-%EC%8B%A4%EC%8A%B5

송신루틴이 보내고 수신될때까지 기다리고, 보내고 수신될때까지 기다리고

기다리다가 수신 루틴이 다 끝나면, 기다렸다가 이제 할려고 하는데 얘도 끝나버림.

3. 동기채널과 다르게 비동기 채널은 버퍼를 이용해 수신 채널이 모든 데이터를 수신하기 전에 버퍼에 데이터를 송신해버리고 자기 일을 할 수 있습니다.