Go: Channel

dev_314·2023년 4월 22일
0

Golang - Trial and Error

목록 보기
3/5

채널

참고
한 눈에 끝내는 고랭 기초 - 채널

package main

import "fmt"

func main() {
	a := 1
	b := 2
	result := 0

	func() {
		result = a + b
	}()

	fmt.Println(result) // 3
}

클로저를 통해 result == 3임을 알 수 있다.
클로저(익명 함수)도 Goroutine으로 호출할 수 있다.

package main

import "fmt"

func main() {
	a := 1
	b := 2
	result := 0

	go func() {
		result = a + b
	}()

	fmt.Println(result) // 0
}

그런데 결과값이 3이 아닌 0이 나온다.
앞서 살펴봤듯, Goroutine이 종료되기를 기다리지 않기 때문이다.

package main

import "fmt"

func main() {
	a := 1
	b := 2
	result := 0

	go func() {
		result = a + b
	}()
    
    // s1. 억지로 Goroutine 끝날때 까지 기다리기
   	// time.Sleep(time.Duration(1000))

	// s2. fmt.Scanln으로 강제로 block
    // s3. sync.WaitGroup 사용하기

	fmt.Println(result) // 0
}

크게 3가지 방법으로 해결할 수 있을 것 같은데, 생각해 보면 세 방법 모두 Goroutine이 끝나기를 기다리는 것이지, Goroutine의 흐름을 제어하는건 아니다.

Channel은 두 용도로 사용된다.

  1. 고루틴 사이에서 값을 주고받는 통로 역할을 하고, 송/수신자가 서로를 기다리는 속성때문에 고루틴의 흐름을 제어함
  2. 채널의 데이터를 주고 받을때까지 해당 고루틴을 종료하지 않아 별도의 lock을 하지 않고도 데이터를 동기화함

Channel은 다음과 같이 사용한다.

package main

import (
	"fmt"
)

func main() {
	a := 1
	b := 2
	result := 0

	channel := make(chan int)

	go func() {
		channel <- a + b
	}()

	result = <-channel

	fmt.Println(result)
}

Goroutine에서 채널에 데이터를 <- 키워드로 송신한다.
Goroutine에서 받은 데이터를 <- 키워드로 변수에 할당할 수 있다. (꼭 변수에 할당해야 하는건 아니다)

중요한 점은 Goroutine의 종료 시점이다.

package main

import (
	"fmt"
)

func main() {
	defer fmt.Println("main function finished")
	a := 1
	b := 2
	result := 0

	channel := make(chan int)

	go func() {
		defer fmt.Println("Goroutine finished")
		channel <- a + b
	}()

	fmt.Println(result)
}

Goroutine A (main)에서 Goroutine B (익명 함수)에서 송신한 데이터를 사용하지 않고 있다.

// 실행 결과
0
main function finished

Goroutine B는 Goroutine A에서 데이터를 수신할 때 까지 기다린다.
그런데 Goroutine A에서 데이터를 수신하지 않았으므로, Goroutine A가 종료되었음에도 Goroutine B는 종료되지 않는다.
따라서 Goroutine finished는 출력되지 않는 것이다.

Deadlock

다음과 같은 상황이 있다.

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

메인 함수에서 채널을 통해, 다른 고루틴으로 데이터를 보내려고한다.
그런데 채널을 통해 데이터를 수신하는 고루틴이 없다.
즉, 다른 고루틴에서 데이터를 수신하지 않으므로 메인 함수 고루틴은 종료되지 않고 Deadlock에 빠지게 된다.

이 예제를 통해, 송/수신을 위한 고루틴을 만들고 수신자와 송신자가 1:1 대응하지 않으면 DeadLock에 빠질 수도 있다는 사실을 알 수 있다.

채널 버퍼

그런데 고루틴에서 채널을 통해 송신하는 데이터를, 다른 고루틴에서 항상 수신하도록 하는건 불편하다.

이를 송신자, 수신자가 채널 버퍼를 통해 데이터를 전송하도록 함으로써 해결할 수 있다.
이미지 출처: 한 눈에 끝내는 고랭 기초 - 비동기 채널과 버퍼

package main
 
import "fmt"
 
func main() {
	channel := make(chan string) // 채널 생성
	buffer := make(chan string, 1) // 크기가 1인 버퍼 생성

	buffer <- "Hello goorm!"
	
	fmt.Println(<-buffer) // 정상적으로 출력 후 종료된다.
}

채널 버퍼는 다음의 규칙을 따른다.

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

import (
	"fmt"
	"sync"
)

func main() {
	wg := new(sync.WaitGroup)
	wg.Add(1)
	buffer := make(chan int, 5)

	go produce(buffer)
	go consume(buffer, wg)
	wg.Wait()
}

func produce(buffer chan int) {
	for i := 0; i < 10; i++ {
		buffer <- i
		fmt.Println("produce: ", i)
	}
}

func consume(buffer chan int, wg *sync.WaitGroup) {
	for i := 0; i < 10; i++ {
		num := <-buffer
		fmt.Println("consume: ", num)
	}
	wg.Done()
}
  1. produceconsume은 각각 별도의 고루틴으로 실행된다.
  2. produce는, consume 여부와 상관없이 루프를 돌면서 값을 넣는다.
    • 그 과정에서 버퍼가 꽉차면 block
  3. consume은 produce 여부와 상관없이 루프를 돌면서 값을 꺼낸다.
    • 그 과정에서 버퍼가 비어있으면 block

그러면 동일한 상황에서, consume하는 데이터의 개수가 더 많으면 어떻게 될까?

package main

import (
	"fmt"
	"sync"
)

func main() {
	wg := new(sync.WaitGroup)
	wg.Add(1)
	buffer := make(chan int, 5)

	go produce(buffer)
	go consume(buffer, wg)
	wg.Wait()
}

func produce(buffer chan int) {
	for i := 0; i < 10; i++ {
		buffer <- i
		fmt.Println("produce: ", i)
	}
}

func consume(buffer chan int, wg *sync.WaitGroup) {
	// 프로듀서는 10개만 생성하는데, 컨슈머는 100개의 데이터를 소비함
	for i := 0; i < 100; i++ {
		num := <-buffer
		fmt.Println("consume: ", num)
	}
	wg.Done()
}

컨슈머는 버퍼가 비어있으면 데이터가 생길 때 까지 기다린다.
그런데 데이터는 10개밖에 생기지 않으므로 결국 consumer는 deadlock에 빠지게 된다.

따라서 송/수신 채널의 개수를 잘 맞춰야한다.

채널 닫기

Deadlock이 발생하는 상황을 정리하자면 다음과 같다.

if Produce 개수 < Consume 개수 {
	Consumer goroutine이 무한 대기
}
package main

import "fmt"

func main() {
	channel := make(chan bool, 2)
	channel <- true
	channel <- true

	fmt.Println(<-channel)
	fmt.Println(<-channel)
	fmt.Println(<-channel) // 비어있는 채널을 읽으면 deadlock 발생
}

위와 같은 상황에서 채널을 닫으면 어떻게 될까

package main

import "fmt"

func main() {
	channel := make(chan string, 2)
	channel <- "hello"
	channel <- "world"

	value, isOpen := <-channel
	fmt.Println(value, isOpen) // hello true
	value, isOpen = <-channel
	fmt.Println(value, isOpen) // world true
	close(channel)
	value, isOpen = <-channel
	fmt.Println(value, isOpen) // "" false
}
  1. 채널에 값이 없는 경우, 닫힌 채널에서 값을 읽으면 데드락이 발생하지 않고 빈 값을 반환한다.

    primitive type이면 기본값
    pointer 등이면 nil
    • 닫힌 채널에 값을 읽는 시도는 할 수 있다.
  2. 닫힌 채널에 값을 넣으면 panic이 발생한다. (send on closed channel)

채널 iterate

무한 루프

package main

import "fmt"

func main() {
	channel := make(chan string, 2)
	channel <- "hello"
	channel <- "world"

	for {
		if value, isOpen := <-channel; isOpen {
			fmt.Println(value, isOpen)
		} else {
			break
		}
	}
}

isOpen이 false가 되려면, 채널이 close되어야만 한다.
그런데 위 코드는 채널이 닫히지 않았으므로 무한 루프, 즉 Deadlock에 빠지게 된다.

그러므로 채널을 for loop돌려면 close 해야 한다.

package main

import "fmt"

func main() {
	channel := make(chan string, 2)
	channel <- "hello"
	channel <- "world"

	close(channel)

	for {
		if value, isOpen := <-channel; isOpen {
			fmt.Println(value, isOpen)
		} else {
			break
		}
	}
}

채널 + for range

채널에 for ragne를 도입해서 개선할 수 있다

package main

import "fmt"

func main() {
	channel := make(chan string, 2)
	channel <- "hello"
	channel <- "world"

	close(channel)

	for value := range channel {
		fmt.Println(value)
	}
}

마찬가지로 채널은 close되어야 한다.
채널에 for range를 사용할 경우, isOpen은 사용할 수 없다.

채널 방향

사실 채널은 방향을 정할 수 있다.

방향 = 송신, 수신 여부

기본적으로 채널은 송신, 수신이 가능한데, 목적에 따라 송신만 가능한 채널, 수신만 가능한 채널로 제한할 수 있다.

package main

func main() {
	bidirectionChannel := make(chan int, 2)
	bidirectionChannel <- 100 // 송신 가능
    <- bidirectionChannel // 수신 가능

	transChan := makeTransChan(make(chan int, 2))
	transChan <- 1 // 송신만 가능
	// result := <- transChan: Invalid operation: <- transChan (receive from the send-only type chan<- int)

	receiveChan := makeReceiveChan(make(chan int, 2))
	// receiveChan <- 1: Invalid operation: receiveChan <- 1 (send to the receive-only type <-chan int)
    <- receiveChan // 수신만 가능
}


func TransOnlyChParam(ch <-chan int) {
	fmt.Println(<-ch)
    // ch <- 100: 수신 전용이므로 송신 불가능
}

func makeTransChan(ch chan int) chan<- int {
	return ch
}

func makeReceiveChan(ch chan int) <-chan int {
	ch <- 2
	return ch
}

방향이 생기더라도 채널, 버퍼가 꽉 차거나, 말랐을 때 발생하는 Deadlock문제는 여전히 발생한다.

Select

아래와 같은 코드가 있다.

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

두 고루틴(a, b)에서 각각 1초, 0.5초 주기로, 채널에 데이터를 전송한다.
그리고 두 채널에서 전송한 데이터를 고루틴(c)에서 수신한다.
그런데 다음과 같은 조건이 있다.

무조건 ch1를 먼저 수신한 다음에 ch2에서 데이터 수신

ch1에 데이터가 없으면 고루틴(c)이 block되어, ch2에 데이터가 더 빨리 많이 쌓이더라도ch2의 데이터를 사용하지 못하는 상황이 발생한다. (ch2도 결국 1초 주기로 데이터가 순환(?)하는 상황)

이를 Select로 개선할 수 있다.

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 {
			select {
			case dataFromCh1 := <-ch1: // case문에서 변수 선언이 가능하다.
				fmt.Println("ch1 수신", dataFromCh1)
			case <-ch2: // 변수 선언 안 해도 상관 없다.
				fmt.Println("ch2 수신")
            case ch1 <- bool: // case문에서 송신도 가능하다.
			}
		}
	}()
    
	time.Sleep(5 * time.Second)
}
  1. select문을 사용함으로써, ch2는 ch1의 수신 여부와 상관없이 데이터를 사용할 수 있게 된다.
  2. ch1, ch2에 데이터가 없다면, 마지막 case문에서 데이터를 송신한다.

오답 노트

아래 코드는 Deadlock에 빠진다. 왜 그럴까

package main

import (
	"fmt"
	"sync"
)

func main() {
	channel := make(chan bool)
	channel <- true

	wg := new(sync.WaitGroup)
	wg.Add(1)

	go func() {
		result := <-channel
		fmt.Println(result)
		wg.Done()
	}()

	wg.Wait()
}
  1. 채널에 데이터를 넣는다.
  2. 채널이 꽉 찼으므로, 메인 고루틴은 block
  3. 그런데 WaitGroup, 익명 함수(고루틴)을 실행하지 않은 상태로 block되었으므로 Deadlock에 빠진다.

다음 처럼 해결할 수 있다.

package main

import (
	"fmt"
	"sync"
)

func main() {
	channel := make(chan bool)
	wg := new(sync.WaitGroup)
	wg.Add(1)
	go func() {
		result := <-channel
		fmt.Println(result)
		wg.Done()
	}()

	channel <- true
	wg.Wait()
}

또는 버퍼를 가득 채우지 않는 방식으로도 해결할 수 있을 것 같다.

package main

import (
	"fmt"
	"sync"
)

func main() {
	channel := make(chan bool, 2)
	channel <- true // 버퍼가 꽉 차지 않았으므로 블록되지 않는다.

	wg := new(sync.WaitGroup)
	wg.Add(1)

	go func() {
		result := <-channel
		fmt.Println(result)
		wg.Done()
	}()

	wg.Wait()
}

이런 상황에서 waitGroup이 없다면, 메인 함수가 익명함수를 기다리지 않고 먼저 종료될 수도 있다.

profile
블로그 이전했습니다 https://dev314.tistory.com/

0개의 댓글