본문 바로가기

Go

[묘공단] Tucker의 Go 언어 프로그래밍 24장~25장

  • 24장 고루틴과 동시성 프로그래밍
  • 25장 채널과 컨텍스트

 

 

24장 고루틴과 동시성 프로그래밍

 

  • Process : 컴퓨터에서 연속적으로 실행되는 컴퓨터 프로그램, 메모리에 올라와 실행되고 있는 프로ㅓ그램의 인스턴스
  • Thread : 프로세스내에서 실행되는 여러 흐름단위
  • 컨텍스트 스위칭 : 실행중인 Thread가 다른 Thread로 교체되는것

 

  • Go언어는 멀티쓰레드를 지원한다, 24장에서는 어떻게 멀티쓰레드를 사용하는지에 대해서 배워볼수있다.
  •  

 

  • 고루팅 사용법은 go 함수호출 로 사용할수있다.
  • 아래 소스에서는 2개의 고루틴을 생성하여 동시에 실행되는 프로그램이다.
//ch24/ex24.1/ex24.1.go
package main

import (
	"fmt"
	"time"
)

func PrintHangul() {
	hanguls := []rune{'가', '나', '다', '라', '마', '바', '사'}
	for _, v := range hanguls {
		time.Sleep(300 * time.Millisecond)
		fmt.Printf("%c ", v)
	}
}

func PrintNumbers() {
	for i := 1; i <= 5; i++ {
		time.Sleep(400 * time.Millisecond)
		fmt.Printf("%d ", i)
	}
}

func main() {
	go PrintHangul()
	go PrintNumbers()

	time.Sleep(3 * time.Second) // ❷ 3초간 대기
}

 

 

 

 

  • 위의 소스에서는 main() 스레드의 종료를 막기위해서 3초간 대기하도록하였다.
  • 하지만 각 쓰레드들이 언제 종료되는 알수없을때는 어떻게 해야될까?
  • WaitGroup객체를 사용하면 각각의 작업이 종료될때까지 대기할수있다.
  • 사용법은 아래와같다.
    • var wg sync.WaitGroup 로 객체를 생성한다. 
    • wg.Add(3) : 작업갯수를 3으로 설정한다.
    • wg.Done() : 작업이 완료될때 호출한다
    • wg.Wait() : 모든 작업이 완료될때까지 대기한다.
//ch24/ex24.2/ex24.2.go
package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup // ❶ waitGroup 객체

func SumAtoB(a, b int) {
	sum := 0
	for i := a; i <= b; i++ {
		sum += i
	}
	fmt.Printf("%d부터 %d까지 합계는 %d입니다.\n", a, b, sum)
	wg.Done() // ❸ 작업이 완료됨을 표시
}

func main() {
	wg.Add(10) // ❷ 총 작업 개수 설정
	for i := 0; i < 10; i++ {
		go SumAtoB(1, 1000000000)
	}

	wg.Wait() // ❹ 모든 작업이 완료되길 기다림.
	fmt.Println("모든 계산이 완료되었습니다.")
}

 

 

 

 

  • 고루틴의 동작방법(코어가 2개일때)
    • 고루틴이 하나일때
      • main()도 하나의 고루틴(스레드)이기때문에 1개의 코어에서 해당 스레드를 처리한다.
    • 고루틴이 두개일때
      • 코어가 2개이기때문에 각각의 코어가 각각의 고루틴(Goroutine)을 처리한다.
    • 고루틴이 3개일때
      • 코어가 2개이기때문에 2개의 고루틴은 실행하고 세번째 고루틴은 남는 코어가 생길때까지 실행하지 않고 대기한다. 만약 2번째 고루틴이 실행완료되면 고루틴2는 사라지고 세번째 고루틴이 실행된다. 
  • 시스템콜 호출시
    • 시스템콜은 운영체제가 지원하는 서비스를 호출할때를 말한다. (네트워크시스템같은..)
    • 시스템콜을 호출하면 운영체제에서는 해당 서비스가 완료될때가지 대기한다. 
    • 이런 대기상태의 고루틴에 cpu코어와 OS스레드를 할당하면 자원낭비가 발생한다.
    • Go언어에서는 이련경우 이런상태의 루틴을 대기상태로 보내고, 실행을 기다리는 다른 고루틴에 cpu코어와 os스레드를 할당하여 실행할수있도록 한다. 
    • 이러한 고언어의 방식때문에 고언어는 컨텍스트스위칭 비용이 발생하지 않는 장점이 있다. 

 

 

 

  • 동시성 프로그래밍 주의점에 대한 예제
    • 아래소스는 동시성 프로그램에 대한 문제점이다. 10개의 고루틴이 account의 통장에 대해서 1000원입급하고 잠깐쉬고(1ms) 1000원을 출금하는 소스이다.
    • 10개의 고루틴이 공통적으로 사용하는 account에 접근하다보니,  잔고가 0원아래로 내려가는 상황이 발생할수있다.
//ch24/ex24.3/ex24.3.go
package main

import (
	"fmt"
	"sync"
	"time"
)

type Account struct {
	Balance int
}

func main() {
	var wg sync.WaitGroup

	account := &Account{0}    // ❶ 0원 잔고 통장
	wg.Add(10)                // ❷ WaitGroup 객체 생성
	for i := 0; i < 10; i++ { // ❸ Go 루틴 10개 생성
		go func() {
			for {
				DepositAndWithdraw(account)
			}
			wg.Done()
		}()
	}
	wg.Wait()
}

func DepositAndWithdraw(account *Account) {
	if account.Balance < 0 { // ➍ 잔고가 0이하면 패닉
		panic(fmt.Sprintf("Balance should not be negative value: %d", account.Balance))
	}
	account.Balance += 1000      // ➎ 1000원 입금
	time.Sleep(time.Millisecond) // ➏ 잠시 쉬고
	account.Balance -= 1000      // ➐ 1000원 출금
}

 

 

 

  • 위의 문제를 해결하기 위하여 뮤텍스라는걸 사용해서 해결할수있다.
  • 뮤텍스를 사용하여, 해당 account에 접근하기 위해 Lock()을 사용하고, 모든 작업이 완료되면 Unlock()를 호출하면, 여러 고루틴이 통장(account)에 동시에 접근하는것을 막을수있다.
//ch24/ex24.4/ex24.4.go
package main

import (
	"fmt"
	"sync"
	"time"
)

var mutex sync.Mutex // ❶ 패키지 전역 변수 뮤텍스

type Account struct {
	Balance int
}

func DepositAndWithdraw(account *Account) {
	mutex.Lock()         // ❷ 뮤텍스 획득
	defer mutex.Unlock() // ❸ defer를 사용한 Unlock()
	if account.Balance < 0 {
		panic(fmt.Sprintf("Balance should not be negative value: %d", account.Balance))
	}
	account.Balance += 1000
	time.Sleep(time.Millisecond)
	account.Balance -= 1000
}

func main() {
	var wg sync.WaitGroup

	account := &Account{0}
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			for {
				DepositAndWithdraw(account)
			}
			wg.Done()
		}()
	}
	wg.Wait()
}

 

  • 뮤텍스를 사용하면 동시성 프로그램의 문제를 해결할수있지만 데드락의 문제가 발생할수있다.
  • 아래소스에서는 의도적으로 데드락을 발생시키는 예제이다.
  • 각각 fork, spoon의 뮤텍스객체를 만들어서 2개의 고루틴에서 수저와 포크를 들어서 밥을 먹는 프로그램이다.
  • 반드시 포크와 수저를 들어야 밥을 먹을수있는데 , 한쪽에서는 수저를 들고, 나머지 한쪽은 포크를 들경우가 발생하면 서로 무한히 기다리는 상태(Deadlock)가 발생한다.
//ch24/ex24.5/ex24.5.go
package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	rand.Seed(time.Now().UnixNano())

	wg.Add(2)
	fork := &sync.Mutex{} // ❶ 포크와 수저 뮤텍스
	spoon := &sync.Mutex{}

	go diningProblem("A", fork, spoon, "포크", "수저") // ❷ A는 포크 먼저
	go diningProblem("B", spoon, fork, "수저", "포크") // ❸ B는 수저 먼저
	wg.Wait()
}

func diningProblem(name string, first, second *sync.Mutex, firstName, secondName string) {
	for i := 0; i < 100; i++ {
		fmt.Printf("%s 밥을 먹으려 합니다.\n", name)
		first.Lock() // ❹ 첫 번째 뮤텍스를 획득 시도
		fmt.Printf("%s %s 획득\n", name, firstName)
		second.Lock() // ➎ 두 번째 뮤텍스를 획득 시도
		fmt.Printf("%s %s 획득\n", name, secondName)

		fmt.Printf("%s 밥을 먹습니다\n", name)
		time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)

		second.Unlock() // ➏ 뮤텍스 반납
		first.Unlock()
	}
	wg.Done()
}

 

 

  • 위의 소스에서 문제를 해결하기위한 방법으로 영역과 역할을 나눌수있다.
  • 예를 들어 여러명의 사람이 하나의 종이에 그린다면,
    • 영역을 나누는 경우 , 종이의 각영역을 각자에게 나눠서 서로의 영역만 그리게하면 된다.
    • 역할을 나누는 경우, 각 역할을 담당하게 하면, 밑그림, 배경스케치,책색을 각자 맡아서 수행하면 서로 간섭없이 그림을 완성할수있다.
  • 아래의 소스는 영역을 나누는 방법에 대한 에제이다.
    • 10가지 작업을 10개의 고루틴이 각각 수행하도록 하여, 각각의 고루틴은 할당된 작업만 하므로 고루틴간의 간섭이 발생하지 않는다.
//ch24/ex24.6/ex24.6.go
package main

import (
	"fmt"
	"sync"
	"time"
)

type Job interface { // ❶ Job 인터페이스
	Do()
}

type SquareJob struct {
	index int
}

func (j *SquareJob) Do() {
	fmt.Printf("%d 작업 시작\n", j.index) // ❷ 각 작업
	time.Sleep(1 * time.Second)
	fmt.Printf("%d 작업 완료 - 결과: %d\n", j.index, j.index*j.index)
}

func main() {
	var jobList [10]Job

	for i := 0; i < 10; i++ { // ❸ 10가지 작업 할당
		jobList[i] = &SquareJob{i}
	}

	var wg sync.WaitGroup
	wg.Add(10)

	for i := 0; i < 10; i++ {
		job := jobList[i] // ❹ 각 작업을 Go 루틴으로 실행
		go func() {
			job.Do()
			wg.Done()
		}()
	}
	wg.Wait()
}

 

 

 

 

 

 

25장 채널과 컨텍스트

 

  • 채널(Channel)이란 고루틴끼리 메세지를 전달할수있는 메세지 큐(Queue)이다.
  • 메세지큐에 메세지들은 차례대로 쌓이게 되고, 메세지를 읽을때는 맨처음 온메세지부터 차례로 읽게된다.(FIFO)

 

  • 채널 인스턴스 생성
    • var message chan string = make(chan string)
  • 채널에 Data 넣기
    • message <- "This is message"
  • 채널에 Data 빼기
    • var msg string = <- message
  • 아래 소스는 채널에 데이터를 넣고 빼는 간단한 예제이다.
  • main에서 채널에 9를 넣고, 고루틴에서 해당 값을 가져온다.
//ch25/ex25.1/ex25.1.go
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	ch := make(chan int) // ❶ 채널 생성

	wg.Add(1)
	go square(&wg, ch) // ❷ Go 루틴 생성
	ch <- 9            // ❸ 채널에 데이터를 넣는다.
	wg.Wait()          // ❹ 작업이 완료되길 기다린다.
}

func square(wg *sync.WaitGroup, ch chan int) {
	n := <-ch // ➎ 데이터를 빼온다

	time.Sleep(time.Second) // 1초 대기
	fmt.Printf("Square: %d\n", n*n)
	wg.Done()
}

 

 

  • 아래 소스는 채널을 생성하고 데이터를 넣었을때, 데이터를 가져가지 않아서, 프로그램이 멈추는 경우이다.
  • 채널에 데이터를 넣었지만 아무도 가져가지 않아서 Block이 되는 경우이다.
//ch25/ex25.2/ex25.2.go
package main

import "fmt"

func main() {
	ch := make(chan int) // ❶ 크기 0인 채널 생성

	ch <- 9                    // ❷ main() 함수가 여기서 멈춘다
	fmt.Println("Never print") // ❸ 실행되지 않는다
}

 

 

  • 위의 소스들은 채널의 크기가 0이기때문에서 수신자와 송신자가 반드시 전송하고, 수신받아야만 된다.
  • 아래 소스는 채널의 크기가 0일경우 발생하는 문제에 대한 예제이다.
  • 크기가 0인 채널을 만들고 채널에 데이타를 넣었지만 보관할곳이 없기때문에 다른 고루틴이 데이터를 빼가기를 기다린다.  고루틴에서 데이타를 계속 기다리면서 채널에서 데이타를 계속 빼간다. 메인쓰레드에서 채널에 모두 값을 넣고 wg.Wait()에서 기다리지만, 고루틴에서는 계속 채널에서 데이타가 오기를 기다리기 때문에 wg.Done()은 영원시 실행되지 않아, Deadlock메세지를 출력하고 프로그램은 종료된다.
//ch25/ex25.3/ex25.3.go
package main

import (
	"fmt"
	"sync"
	"time"
)

func square(wg *sync.WaitGroup, ch chan int) {
	for n := range ch { // ❷ 데이터를 계속 기다린다.
		fmt.Printf("Square: %d\n", n*n)
		time.Sleep(time.Second)
	}
	wg.Done() // ❹ 실행되지 않는다.
}

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)

	wg.Add(1)
	go square(&wg, ch)

	for i := 0; i < 10; i++ {
		ch <- i * 2 // ❶ 데이터를 넣는다.
	}
	wg.Wait() // ❸ 작업 완료를 기다린다.
}

 

 

  • 위의 문제를 해결하기 위해서 메인스레드에서 채널에 모두 값을 넣은후에 close(ch)로 채널을 닫는다.
  • 고루틴에서는 채널이 닫혔기때문에 루프를 빠져나오고 wg.Done()을 실행하여 프로그램이 정상종료된다. 
//ch25/ex25.4/ex25.4.go
package main

import (
	"fmt"
	"sync"
	"time"
)

func square(wg *sync.WaitGroup, ch chan int) {
	for n := range ch { // ❷ 채널이 닫히면 종료
		fmt.Printf("Square: %d\n", n*n)
		time.Sleep(time.Second)
	}
	wg.Done()
}

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)

	wg.Add(1)
	go square(&wg, ch)

	for i := 0; i < 10; i++ {
		ch <- i * 2
	}
	close(ch) // ←- ❶ 채널을 닫는다.
	wg.Wait()
}

 

 

 

 

  • 만약에 채널이 여러개면 어떻게 처리할까? Go에서는 Select문을 사용하여 여러채널을 동시에 대기하게 할수있다.
  • 아래의 소스에서 select를 사용하여, ch와 quit의 두개의 채널을 대기하고 있다. 해당 소스는 ch에 메세지가 오면 출력하고 quit채널에 메세지가 오면 고루틴을 종료하는 소스이다. 
//ch25/ex25.5/ex25.5.go
package main

import (
	"fmt"
	"sync"
	"time"
)

func square(wg *sync.WaitGroup, ch chan int, quit chan bool) {
	for {
		select { // ❷ ch와 quit 양쪽을 모두 기다린다.
		case n := <-ch:
			fmt.Printf("Square: %d\n", n*n)
			time.Sleep(time.Second)
		case <-quit:
			wg.Done()
			return
		}
	}
}

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)
	quit := make(chan bool) // ❶ 종료 채널

	wg.Add(1)
	go square(&wg, ch, quit)

	for i := 0; i < 10; i++ {
		ch <- i * 2
	}

	quit <- true
	wg.Wait()
}

 

 

  • 아래소스는 3개의 채널을 사용하고 있다.
    • tick : 1 간격으로 신호를 보내주는 채널이다. time.Tick()함수가 일정시간간격주기로 신호를 보내주는  채널을 생성해서 반환한다.
    • terminate : time.After()함수는 일정시간 후에 신호를 보내주는 채널을 채널을 생성해서 반환하는 함수이다.
    • ch : main()루프에서 생성한 채널이다. 
  • 실행을 하면 일정시간마다 tick이 실행되고, main()에서 보내주는 ch을 출력하면서, 10초후 terminate채널이 실행되어 프로그램이 종료된다. 
//ch25/ex25.6/ex25.6.go
package main

import (
	"fmt"
	"sync"
	"time"
)

func square(wg *sync.WaitGroup, ch chan int) {
	tick := time.Tick(time.Second)            // ❶ 1초 간격 시그널
	terminate := time.After(10 * time.Second) // ❷ 10초 이후 시그널

	for {
		select { // ❸ tick, terminate, ch 순서로 처리
		case <-tick:
			fmt.Println("Tick")
		case <-terminate:
			fmt.Println("Terminated!")
			wg.Done()
			return
		case n := <-ch:
			fmt.Printf("Square: %d\n", n*n)
			time.Sleep(time.Second)
		}
	}
}

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)

	wg.Add(1)
	go square(&wg, ch)

	for i := 0; i < 10; i++ {
		ch <- i * 2
	}
	wg.Wait()
}

 

 

  • 아래소스는 채널을 사용하여 생산자 소비자 패턴을 구현하는 소스이다.
  • 차를 생산하기 위해서 차체생산, 바퀴설치, 도색의 3단계를 거친다. 각단계마다 1초가 걸린다고 했을때, 첫차가 생산되는데 3초가 경과되지만 그이후로는 1초간격으로 차가 완성된다. 
  • MakeBody()루틴이 생산자, InstallTire() 소비자이다.  또 InstallTire()는 PaintCard()루틴에 대해서는 생산자가 되는구조이다. 
//ch25/ex25.7/ex25.7.go
package main

import (
	"fmt"
	"sync"
	"time"
)

type Car struct {
	Body  string
	Tire  string
	Color string
}

var wg sync.WaitGroup
var startTime = time.Now()

func main() {
	tireCh := make(chan *Car)
	paintCh := make(chan *Car)

	fmt.Printf("Start Factory\n")

	wg.Add(3)
	go MakeBody(tireCh) // ❶ Go 루틴 생성
	go InstallTire(tireCh, paintCh)
	go PaintCar(paintCh)

	wg.Wait()
	fmt.Println("Close the factory")
}

func MakeBody(tireCh chan *Car) { // ❷ 차체 생산
	tick := time.Tick(time.Second)
	after := time.After(10 * time.Second)
	for {
		select {
		case <-tick:
			// Make a body
			car := &Car{}
			car.Body = "Sports car"
			tireCh <- car
		case <-after: // ❸ 10초 뒤 종료
			close(tireCh)
			wg.Done()
			return
		}
	}
}

func InstallTire(tireCh, paintCh chan *Car) { // ❹ 바퀴 설치
	for car := range tireCh {
		// Make a body
		time.Sleep(time.Second)
		car.Tire = "Winter tire"
		paintCh <- car
	}
	wg.Done()
	close(paintCh)
}

func PaintCar(paintCh chan *Car) { // ➎ 도색
	for car := range paintCh {
		// Make a body
		time.Sleep(time.Second)
		car.Color = "Red"
		duration := time.Now().Sub(startTime) // ➏ 경과 시간 출력
		fmt.Printf("%.2f Complete Car: %s %s %s\n", duration.Seconds(), car.Body, car.Tire, car.Color)
	}
	wg.Done()
}

 

 

  • 컨텍스트는 context 패키지에서 제공하는 기능으로, 작업을 지시할때 작업가능시간, 작업취소등의 조건을 지시하는 작업명세서 역할을 한다.
  • 아래소스에서 보면 ctx, cancel := context.WithCancel(context.Background()) 로 컨텍스트를 생성한다.
  • 고루틴에서 ctx는 select문으로 계속 확인하고, main()에서 cancel()이 호출되면, 컨텍스트의 Done()채널에서 시그널을 보내 , 고루틴이 종료되는 소스이다. 
//ch25/ex25.8/ex25.8.go
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx, cancel := context.WithCancel(context.Background()) // ❶ 컨텍스트 생성
	go PrintEverySecond(ctx)
	time.Sleep(5 * time.Second)
	cancel() // ❷ 취소

	wg.Wait()
}

func PrintEverySecond(ctx context.Context) {
	tick := time.Tick(time.Second)
	for {
		select {
		case <-ctx.Done(): // ❸ 취소 확인
			wg.Done()
			return
		case <-tick:
			fmt.Println("Tick")
		}
	}
}

 

 

  • 아래 소스는 ctx := context.WithValue(context.Background(), "number", 9) 으로 컨텍스트에 값을 추가한다.
  • 고루틴에서는 number키로 9를 읽어와서 출력하고 wg.Done() 수행하고 종료한다.
//ch25/ex25.9/ex25.9.go
package main

import (
	"context"
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)

	ctx := context.WithValue(context.Background(), "number", 9) // ❶ 컨텍스트에 값을 추가한다
	go square(ctx)

	wg.Wait()
}

func square(ctx context.Context) {
	if v := ctx.Value("number"); v != nil { // ❷ 컨텍스트에서 값을 읽어온다.
		n := v.(int)
		fmt.Printf("Square:%d", n*n)
	}
	wg.Done()
}