정상우
hELLO.
정상우
전체 방문자
384,444
오늘
63
어제
1,174
  • hELLO. (121)
    • 컴퓨터과학 (4)
      • 알고리즘 & 자료구조 (4)
    • 언어 & 프레임워크 (63)
      • Go (23)
      • PHP & Laravel (40)
    • 웹 (7)
    • 블록체인 (12)
      • 메인넷 (9)
      • 암호화폐 플랫폼 (3)
    • 포트폴리오 (10)
    • 칼럼 (20)
      • 에세이 (4)
      • 개발자스럽게 살기 (14)
      • 회고 (2)
    • 티스토리 (5)

블로그 메뉴

  • ⚡ 개발자 이력서
  • 🌟 깃허브
  • 💻 강의
  • ✨ 예제코드
  • ⭐ 브런치
  • 태그 클라우드
  • 방명록

공지사항

  • 2차 도메인을 설정했습니다 ✨

인기 글

  • JWT(JSON Web Token)의 개념부⋯
    2021.07.29
    JWT(JSON Web Token)의 개념부⋯
  • 'REST' 를 보다 'RESTful' 하게⋯
    2021.08.14
    'REST' 를 보다 'RESTful' 하게⋯
  • [Laravel] 라라벨 프레임워크⋯
    2021.06.10
    [Laravel] 라라벨 프레임워크⋯
  • 깃허브를 포트폴리오로 쓰려면⋯
    2021.12.25
    깃허브를 포트폴리오로 쓰려면⋯
  • 암호화폐 트레이딩 봇을 만들었⋯
    2021.05.12
    암호화폐 트레이딩 봇을 만들었⋯

태그

  • 라라벨
  • go
  • 포트폴리오
  • 개발 리뷰
  • 개발
  • php
  • 블록체인
  • 프로그래머스
  • Algorithm
  • 코딩테스트

최근 댓글

  • 고맙습니다 ~^^
    정상우
  • 오늘 블로그 만들었는데 검색하⋯
    엥뿌삐
  • 좋은 스킨 정말 감사드립니다.⋯
    이태홍
  • 고맙습니다 ㅎㅎ
    정상우
  • 제가 원하던 최고의 스킨입니다⋯
    _HEON

최근 글

  • 빠르게 성장하는 개발자의 세⋯
    2022.06.08
    빠르게 성장하는 개발자의 세⋯
  • 개발자와 엔지니어, 그 사이에서
    2022.05.10
    개발자와 엔지니어, 그 사이에서
  • 아임포트(Iamport)로 결제기능⋯
    2022.04.03
    아임포트(Iamport)로 결제기능⋯
  • 아임포트(Iamport)로 결제기능⋯
    2022.04.01
    아임포트(Iamport)로 결제기능⋯
  • [Laravel] 카페24 호스팅에 라⋯
    2022.03.29
    [Laravel] 카페24 호스팅에 라⋯

티스토리

hELLO · Designed By 정상우.
정상우

hELLO.

언어 & 프레임워크/Go

Go: 고루틴과 채널 (go-routine, chan)

2020. 12. 9. 20:14

Go 에서 동시성을 제어하는 일은 너무나도 쉽다. 너무 쉬워서 남발하는 경우까지 나올 정도로 쉬운데, Go 의 동시성은 고루틴(go-routine)이라고 하는데, 이는 OS 스레드를 한 번 더 추상화한 코루틴(co-routine)이다. 이러한 동시성은 멀티 프로세서를 사용하여 실행하거나 문맥교환(Context-Switching)을 통해 동시에 실행되는 것처럼 행동한다. 고루틴과 채널에 대한 내용은 상당히 길기때문에 해당 포스팅에서 전부 이야기할 수는 없을 것이라 여기서는 기초적인 것만 이야기하고, 더 자세한 것은 이후의 포스트에서 다룰 예정이다.

go & go-routine

main() 함수도 사실은 main() 고루틴이다. 이러한 고루틴을 실행하는 것은 아주 간단한데, 단순히 그냥 go 키워드만 붙여주면 된다. 고루틴을 설명함에 있어 한 가지 예시를 들어볼텐데, 아래의 코드는 웹페이지에 Get 요청을 하고 내용을 반환해준다. 3번의 요청을 직렬로 처리하기 때문에 특정 웹페이지의 응답이 늦어지거나 하면 프로그램이 늦게 끝날수도 있다.

package main

import (
	"io/ioutil"
	"log"
	"net/http"
)

func GetWebpageContent(url string) string {
	response, err := http.Get(url)
	if err != nil {
		log.Fatal(err)
	}
	defer response.Body.Close()
	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		log.Fatal(err)
	}

	return string(body)
}

func main() {
	GetWebpageContent("https://example.com")
	GetWebpageContent("https://golang.org")
	GetWebpageContent("https://golang.org/doc")
}

그럼 이걸 각자 다른 고루틴에서 실행하게 만들면, 동시성 코드를 만들 수 있는데, go 키워드를 붙여주기만 하면 된다고 했으니 해보도록 하자. 그런데 여기서 살펴보아야 하는 점은, main() 고루틴이 끝나면 다른 고루틴의 실행여부와는 관계 없이 프로그램이 끝난다는 것이다.

package main

import (
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

func GetWebpageContent(url string) string {
	// ...
}

func main() {
	go GetWebpageContent("https://example.com")
	go GetWebpageContent("https://golang.org")
	go GetWebpageContent("https://golang.org/doc")

	time.Sleep(time.Second * 5)
}

time.Sleep() 을 사용하면 다른 고루틴이 끝날때까지 대기할 수 있다. 하지만 이렇게하면 고루틴이 5초이내로 끝난다고해도 결국 5초를 기다려야하고, 응답이 5초를 넘어선다면 받지도 못하고 프로그램이 끝나버리게 될 것이다.

sync.WaitGroup

time.Sleep() 대신에 사용할 수 있는것이 바로 sync.WaitGroup 다. 이것을 사용하게 되면 main() 고루틴은 다른 고루틴이 끝날 때까지 대기할 수 있게되고 등록해둔 고루틴들이 종료되면 main() 고루틴도 종료되면서 프로그램이 끝날 것이다.

package main

import (
	"io/ioutil"
	"log"
	"net/http"
	"sync"
)

func GetWebpageContent(url string) string {
	defer wg.Done()
	// ...
}

var wg sync.WaitGroup

func main() {
	wg.Add(3)
	go GetWebpageContent("https://example.com")
	go GetWebpageContent("https://golang.org")
	go GetWebpageContent("https://golang.org/doc")

	wg.Wait()
}

wg.Add() 에 들어가는 값은 실행할 고루틴의 수이며 wg.Done() 을 실행하면 해당 고루틴을 종료한다는 의미이며 wg.Add() 에 넣은 고루틴의 수와 wg.Done() 을 호출한 고루틴의 수는 같아야 한다. 마지막으로 wg.Wait() 를 호춣게 되면 main() 고루틴 이외에 다른 고루틴이 끝날 때까지 기다린다.

채널

고루틴은 언제 끝날지 예측이 안 되기 때문에 값을 반환하고 그 값을 사용하는 방식으로 사용할 수 없다. 그래서 고루틴간의 값을 주고받을 방식이 필요한데 Go 에서는 채널이라는 방식을 택했다. TV채널에서 알 수 있듯이 채널이라는 개념은 단방향 소통방식이다. 방송사가 채널을 통해 방송을 송출하면 불특정 다수의 사람이 받을 수 있는데, Go 의 채널도 마찬가지다. 어떤 고루틴이 채널을 통해 데이터를 보내면 또 다른 고루틴이 데이터를 받아서 사용할 수 있다. 이는 고루틴 간의 소통 방식이라서 main() 고루틴과 타 고루틴간의 통로가 되어준다.

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

func GetWebpageContent(url string) {
	// ...
	ch <- string(body)
}

var ch chan string = make(chan string)

func main() {
	defer close(ch)

	urls := []string{"https://example.com", "https://golang.org", "https://golang.org/doc"}
	for _, url := range urls {
		go GetWebpageContent(url)
	}
	for i := 0; i < len(urls); i++ {
		fmt.Println(<-ch)
	}
}

ch 변수의 chan string 이 바로 채널이다. chan 바로 다음에 채널에 들어갈 데이터의 타입을 주면 된다. 채널은 make() 를 통해 생성해주어야 한다. 채널을 닫으려면 빌트인 함수인 close() 를 사용하면 된다. 채널에 데이터를 넣고 받는 연산은 위의 코드에서 볼 수 있으나 한 가지 의문이 들 수 있다면 sync.WaitGroup 을 사용하지 않았다는 점이다.

 

채널은 데이터를 송신하고 또 다른 고루틴에서 데이터를 수신하지 않으면 해당 고루틴은 블로킹되어 멈추게 된다. 3개의 고루틴 중에 가장 먼저 끝난 고루틴이 채널에 데이터를 넣었으나 아직 수신을 하지 않아 블로킹되어 있으며, 마찬가지로 다음으로 끝난 고루틴도 요청을 다 마쳤다고 하더라도 채널에 이미 데이터가 들어가 있는 상태이기 때문에 다른 고루틴에서 수신이 되는대로 데이터를 채널에 보낼 준비를 하고 있다.

데이터를 지속적으로 수신하기

닫히지 않은 채널에 대해 데이터를 지속적으로 수신하고 싶다면 for ~ range 를 채널에 대해 적용시키면 된다. 이렇게하면 채널이 닫힐때까지 데이터를 계속 수신하려든다. 아래의 코드는 기존의 코드에서 약간 수정되었는데, 기존에 단일로 받던 함수에 대해 배열로 받도록 바꾸었다. 채널 송신에 대해 블로킹을 당하다가 모든 채널이 다 수신되면 채널을 닫는다.

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

func GetWebpageContents(urls []string) {
	for _, url := range urls {
		// ...
		ch <- string(body)
	}
	close(ch)
}

var ch chan string = make(chan string)

func main() {
	urls := []string{"https://example.com", "https://golang.org", "https://golang.org/doc"}
	go GetWebpageContents(urls)

	for b := range ch {
		fmt.Println(b)
	}
}

다수의 채널에서 데이터가 있는 채널 수신하기

다수의 채널을 리스닝하면서 특정 시점에 송신된 데이터가 있는 채널에 대해 수신하여 처리할 수 있는데, 이것이 바로 for + select 구문이다. for 문과 select 문을 섞어서 사용하며, select 문은 다수의 채널에서 데이터가 송신된 채널에 대해 선택하여 수신한다. select 만 단독으로 사용하는 경우는 채널을 한 번만 수신하며 무한 루프와 함께 사용하면 대기하면서 채널을 선택하여 수신할 수 있다. 물론 이러한 경우는 대체로 별로 없다.

package main

import (
	"fmt"
)

func Counter()  {
	ch <- 10
}

var ch chan int = make(chan int)

func main() {
	go Counter()

	select {
	case count := <-ch:
		fmt.Println(count)
	}
}

위의 코드는 ch 채널에 대해 수신을 할 수 있다고 이야기한다. 이번에는 채널을 두 개 써보도록 하자. 우리의 프로그램은 5초 뒤에 동작을 멈추게 될 것이다. Counter() 고루틴은 1초마다 카운트를 올려 채널에 전송한다. 제한은 없으므로 무한루프다. 그러나, Stop() 고루틴으로 인해 5초 뒤 select 에서 quit 채널에 대해 수신하고 프로그램을 종료한다.

package main

import (
	"fmt"
	"os"
	"time"
)

func Counter()  {
	for i := 0;; i++ {
		time.Sleep(time.Second)
		ch <- i
	}
}

func Stop()  {
	time.Sleep(time.Second * 5)
	quit <- true
}

var ch chan int = make(chan int)
var quit chan bool = make(chan bool)

func main() {
	go Counter()
	go Stop()

	for {
		select {
		case count := <-ch:
			// 0, 1, 2, 3
			fmt.Println(count)
		case <-quit:
			os.Exit(1)
		}
	}
}
    '언어 & 프레임워크/Go' 카테고리의 다른 글
    • Go: flag 패키지로 CLI 도구 만들기
    • Go: Go 답게 코드 작성하기
    • Go: 에러와 패닉 (panic, recover)
    • Go: 인터페이스와 타입 단언 (Interface, Type Assertion)
    CHAN, go, go-routine, 고루틴, 동시성, 채널
    정상우
    정상우
    과거의 배움으로 현재를 바꾸고 미래를 만듭니다. #25+2살 #INFJ #개발자 #브런치작가
    댓글쓰기
    1. 테샤르
      2020.12.10 10:53 신고
      잘보고갑니다 .
      수정/삭제댓글쓰기댓글보기
    다음 글
    Go: Go 답게 코드 작성하기
    이전 글
    Go: 에러와 패닉 (panic, recover)
    • 이전
    • 1
    • ···
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • ···
    • 23
    • 다음

    티스토리툴바