ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 8장 고루틴과 채널
    프로그래밍/Golang 2019. 11. 13. 22:26
    The Go Programming Languag

    개요

    2가지 스타일의 동시성 프로그래밍 스타일을 지원

    독립적인 작업(고루틴) 간에 값을 전달

    변수는 대부분 단일 작업에 국한되는 모델인 CSP(상호 통신하는 연속된 프로세스)를 지원하는 고루틴과 채널

    8.1 고루틴

    동시에 수행되는 작업을 고루틴이라 함

    함수 호출이 순차적이지 않고 두 함수의 호출이 동시에 일어남

    스레드와 고루틴은 차이가 있음

    프로그램이 시작한 뒤 고루틴은 main 함수를 호출하는 것이며 메인 고루틴이라 함

    새 고루틴은 go문에 의해 생성

    문법적으로 go문은 키워드 go가 앞에 붙는 일반 함수 또는 메소드 호출

    go문은 함수가 새로 만든 고루틴에서 호출되게 함

    go문 자체는 즉시 완료됨

    f() // call f(); wait for it to return
    go f() // create a new goroutine that calls f(); don't wait

    한 고루틴이 다른 고루틴을 종료시키는 방법은 없음

    고루틴이 스스로 종료하도록 요청하는 방법은 있음

     

    8.2 예제: 동시 시계 서버

    시계 서버는 한 연결에 하나의 고루틴을 사용

     

    8.3 예제: 동시 에코 서버

    에코 서버는 연결마다 여러 고루틴을 사용

     

     

    8.4 채널

    고루틴이 Go 프로그램의 동작이라면 채널(channel)은 고루틴 간의 연결

    채널은 한 고루틴이 다른 고루틴으로 값을 보내기 위한 통신 메커니즘

    각 채널은 채널의 요소 타입이라는 특정 타입 값의 통로

    다음은 chan int 채널을 생성하는 방법

    ch := make(chan int) // ch has type 'chan int'

    채널은 맵과 마찬가지로 make로 생성된 데이터 구조에 대한 참조

    채널을 복사하거나 함수의 인자로 전달할 때는 참조를 복사하기 때문에 호출자와 피호출자는 같은 데이터 구조를 참조

    채널의 제로 값은 nil

    채널은 == 연산자로 비교 가능

    채널은 3개의 작업 요소가 있으며 송신과 수신, close

    송신 구문은 한 고루틴에서 채널을 통해 그에 대응하는 수신 표현식이 있는 다른 고루틴으로 값을 전달

    송신과 수신은 <- 연산자로 작성

    송신 구문에서는 <-로 채널과 값 피연산자를 분리

    수신 표현식에서는 <-가 채널 피연산자 앞에 위치

    결과를 사용하지 않는 수신 표현식도 유효

    ch <- x // a send statement
    x = <-ch // a receive expression in an assignment statement 
    <-ch // a receive statement; result is discarded
    

    close는 이 채널이 더 이상 값을 보내지 않음을 나타내는 플래그를 설정

    이후 송신을 시도하면 패닉이 발생

    채널을 닫으려면 내장된 close 함수를 호출

    close(ch)

    기본 make 호출로 생성된 채널은 버퍼 없는 채널이자만 make 함수에 부가적인 두 번째 정수 인자로 채널의 용량을 지정 가능

    ch = make(chan int) // unbuffered channel
    ch = make(chan int, 0) // unbuffered channel
    ch = make(chan int, 3) // buffered channel with capacity 3

    8.4.1 버퍼 없는 채널

    버퍼 없는 채널에 대한 송신 작업은 수신 고루틴이 재개할 수 있을 때까지 보내는 고루틴을 중단

    수신 작업이 먼저 시도됐다면 수신하는 고루틴은 다른 고루틴이 송신을 수행할 때까지 중단

    버퍼 없는 채널에서 통신은 송신과 수신 고루틴이 동기화되게 함

    버퍼 없는 채널은 동기 채널이라고 함

     

    x가 y 이전에 발생한다면 y는 x 값에 의존할 수 있음

    x가 y 이전에 발생하지 않고 이후에 발생하지도 않는다면 x, y가 동시(concurrent)에 일어난다고 말함

     

    8.4.2 파이프라인

    파이프라인은 채널을 통해 한 고루틴의 출력을 다른 고루틴의 입력으로 연결

     

    Counter 고루틴은 정수 0, 1, 2 등을 생성하고 이 정수를 채널을 통해 squarer 고루틴으로 보냄

    squarer 고루틴은 받은 값들을 제곱하고 결과를 printer 고루틴으로 보냄

    printer 고루틴은 제곱 값을 받아서 출력

    gopl.io/ch8/pipeline1
    func main() {
    	naturals := make(chan int) 
        squares := make(chan int)
        // Counter
        go func() {
    		for x := 0; ; x++ { 
            	naturals <- x
    		} 
        }()
        // Squarer
        go func() {
    		for {
    			x := <-naturals
    			squares <- x * x 
            }
    	}()
        // Printer (in main goroutine)
        for {
    		fmt.Println(<-squares) 
        }
    }

    8.4.3 단방향 채널 타입

    보내기 동작이나 받기 동작 중 한 가지만 노출하는 단방향 채널 타입을 제공

    chan<- int 타입은 int를 보내기만 하는 채널로 보내기는 허용하지만 받기는 허용하지 않음

    <-chan int 타입은 int를 받기만 하는 채널로 받기는 허용하지만 보내기는 허용하지 않음(chan 키워드에 상대적인 화살표 <-의 위치를 기억)

    이 원칙을 위반하면 컴파일시 감지됨

    close 동작은 채널에 더 이상의 송신이 일어나지 않을 것을 가정하고 이 작업은 보내는 고루틴에서만 호출할 수 있기 때문에 받기 전용 채널을 닫으려고 하면 컴파일시 오류가 발생

    gopl.io/ch8/pipeline3
    func counter(out chan<- int) { 
    	for x := 0; x < 100; x++ {
    		out <- x 
        }
    	close(out) 
    }
    func squarer(out chan<- int, in <-chan int) { 
    	for v := range in {
    		out <- v * v 
        }
    	close(out) 
    }
    func printer(in <-chan int) { 
    	for v := range in {
    		fmt.Println(v) 
        }
    }
    func main() {
    	naturals := make(chan int) 
    	squares := make(chan int)
        
    	go counter(naturals)
    	go squarer(squares, naturals)
    	printer(squares)
    }

    양방향 채널에서 단방향 채널 타입으로 변환은 어떤 할당문에서든 가능하지만 되돌릴 수는 없음

    한번 chan<- int와 같은 단방향 타입 값이 되면 이 값에서 같은 채널 데이터 구조를 참조하는 chan int 타입의 값을 얻는 방법은 없음

     

    8.4.4 버퍼 채널

    버퍼 채널은 요소의 큐(queue)를 갖고 있음

    이 큐의 최대 크기는 make 함수로 만들 때 용량 인자에 의해 결정

    다음 구문은 3개의 string 값을 유지할 수 있는 버퍼 채널

     

    버퍼 채널에서 송신 작업은 큐의 뒤쪽으로 요소를 삽입하고 수신 작업은 큐의 앞족에서 요소를 제거

    채널이 가득 찬 경우 송신 작업은 다른 고루틴의 수신 작업으로 공간이 생길 때까지 대기

    채널이 비어있는 경우 수신 작업은 다른 고루틴에서 값이 송신될 때까지 대기

    이 채널에는 값을 3개까지 대기 없이 보낼 수 있음

    ch <- "A" 
    ch <- "B" 
    ch <- "C"

    이때 채널은 가득 차고 네 번째 송신 구문은 대기하게 됨

    값을 1개 받으면 다음과 같음

    fmt.Println(<-ch) // "A"

     

     

    버퍼 없는 채널은 수신자가 없는 채널로 응답을 송신하는 과정에서 막히면 고루틴 유출(goroutine leak)이 발생하며 이는 버그

    쓰레기 변수와 달리 유출된 고루틴은 자동으로 수집되지 않으므로 더 이상 필요하지 않은 고루틴은 반드시 스스로 종료하게 해야 함

     

    채널 버퍼링은 지연되는 현상이며 초기 단계가 다음 단계보다 계속 빠르게 유지되면 그 사이의 버퍼는 항상 가득차 있고 반대로 후기 단계가 빠르다면 버퍼는 늘 비어 있이서 지연이 발생

     

    8.5 병렬 루프

    루프의 모든 반복을 병렬로 실행하는 동시성 패턴이 있음

    서로 완전히 독립적인 하위 문제들로 구성된 문제는 처치 곤란 병렬(embarassingly parallel)이라고 함

    처치 곤란 병렬 문제는 동시성을 구현하고 병렬 처리의 양에 따라 선형적으로 확장되는 성능을 보기에 가장 쉬움

    다음은 여러 고루틴에서 안전하게 변경할 수 있고, 0이 될 때까지 기다리게  할 수 있는 sync.WaitGroup 타입

    // makeThumbnails6 makes thumbnails for each file received from the channel. 
    // It returns the number of bytes occupied by the files it creates.
    func makeThumbnails6(filenames <-chan string) int64 {
    	sizes := make(chan int64)
    	var wg sync.WaitGroup // number of working goroutines 
        for f := range filenames {
    		wg.Add(1)
    		// worker
    		go func(f string) {
    			defer wg.Done()
    			thumb, err := thumbnail.ImageFile(f) 
                if err != nil {
    				log.Println(err)
    				return 
                }
    			info, _ := os.Stat(thumb) // OK to ignore error
    			sizes <- info.Size() 
            }(f)
        }
        // closer
        go func() {
            wg.Wait()
            close(sizes)
        }()
        var total int64
        for size := range sizes {
            total += size 
        }
        return total
    }

    위 코드의 구조는 반복 횟수를 모르는 병렬 루프에서 일반적이고 이상적인 패턴

    Add, Done 메소드는 서로 비대칭

    카운터를 증가시키는 Add는 작업자의 고루틴 안이 아닌 고루틴을 시작하기 전에 호출해야 함

    그렇지 않으면 Add가 반드시 "closer" 고루틴이 Wait을 호출하기 전에 실행되게 할 수 없음

    Add는 파라미터를 받지만 Done은 받지 않음

    Done은 Add(-1)과 같음

    defer를 사용해 오류가 발생할 때에도 카운터가 감소하게 함

     

    세로줄은 고루틴

    얇은 부분은 대기 구간

    두꺼운 부분은 활동 구간

    대각선 화살표는 이벤트가 동기화된 고루틴들

    시간은 아래로 흐름

    메인 고루틴은 range 루프에서 작업자가 값을 보내거나 closer가 채널을 닫기를 기다리는 데 대부분의 시간을 보낸다는 것을 알 수 있음

     

    8.6 예제: 동시 웹 크롤러

     

    8.7 select를 통한 다중화

    select 구문을 사용해 다중화(multiplexing)해야 함

    select { 
    case <-ch1:
    	// ...
    case x := <-ch2:
    	// ...use x... 
    case ch3 <- y:
    	// ... 
    default:
    	// ... 
    }

    각 case는 통신(특정 채널에서 송신 또는 수신 작업)과 관련된 구문 블록을 지정

    수신 표현식은 첫 번째 case에서처럼 독자적으로 나타내거나 두 번째 케이스에서처럼 잛은 변수 선언으로 나타냄

    두 번째 형태로 수신된 값을 참조 가능

    select는 일부 case의 통신이 진행할 수 있을 때까지 대기하고 그 후 해당 통신을 수행하며 case에 연관된 문장을 실행

    case가 없는 select인 select{}는 영구히 대기

    여러 case가 준비 상태라면 select는 임의로 하나를 선택해 모든 채널이 같은 비율로 선택되게 함

    select는 다른 모든 통신을 즉시 수행할 수 없을 때 실행되는 default 케이스를 가질 수 있음

     

    채널 폴링(channel pooling)은 주기적으로 수행하는 것

     

    8.8 예제: 동시 디렉토리 탐색

    8.9 취소

    현재 수행 중인 작업을 중지하게 지시할 필요가 있으며 예르 들어 클라이언트 대신 계산을 수행하는 웹 서버에서 클라이언트의 접속이 끊긴 경우

    임의의 개수의 고루틴을 취소할 때는 취소할 고루틴 개수만큼의 이벤트를 보내는 것

    또는 채널에 값을 보내는 대신 닫는 것

    다음은 호출 시점의 취소 상태를 확인하거나 폴링하는 유틸리티 함수인 cancelled를 정의

    gopl.io/ch8/du4
    var done = make(chan struct{})
         func cancelled() bool {
            select {
    	case <-done: 
        		return true
            default:
    		return false
    	} 
    }

     

    8.10 예제: 채팅 서버

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    '프로그래밍 > Golang' 카테고리의 다른 글

    10장 패키지와 Go 도구  (0) 2019.11.20
    9장 공유 변수를 이용한 동시성  (0) 2019.11.20
    Golang 관련 도서 다운로드 및 번역 문서  (0) 2019.11.13
    7장 인터페이스  (0) 2019.11.06
    6장 메소드  (0) 2019.11.04
Designed by Tistory.