본문 바로가기
IT 초보코딩의 세계/Go 언어

Go언어의 채널(Channel), Select 구문, Sync 패키지 2장

by 조이럭키7 2023. 4. 4.
반응형

동시성과 고루틴, 데이터 공유에 대해서 학습하지 않았다면 아래포스팅을 다시한번 보고 오자.

https://joylucky7.tistory.com/33

 

Go언어의 동시성 과 Goroutine, 데이터공유 1장

◆ 동시성(Concurrency) 란? ▶ 프로그램을 여러 독립된 작은 단위로 나누고 주어진 자원을 사용해 빠르게 동시다발적으로 수행하는 행위 ▶ 동시성의 개념은 스레드보다 더 포괄적 ▶ 스레드 ● 스

joylucky7.tistory.com


◆ 일반 채널 - 동기 채널

선언

make(chan 데이터의 자료형)

채널에 데이터 저장

채널 <- 데이터

채널을 통해서 값 전달 받기

 변수:= <- 채널

채널을 통해 데이터를 받거나 보내면 해당 고루틴은 송수신이 완료될 때까지 대기

데이터를 수신할 다른 고루틴이 없을 경우 고루틴은 대기하고 반대로 데이터를 받아야 하는데 보내는 고루틴이 없다면 기다려야 함

이 방식은 데이터를 동기화해 최신 상태로 유지할 수 있으며 다른 프로그래밍 언어에서 사용하는 잠금 방식보다 단순

package main

import (
  "fmt"
  "time"
)

func runLoopSend(n int, ch chan int) {
  for i := 0; i < n; i++ {
  ch <- i
  }
  close(ch)
}

func runLoopReceive(ch chan int) {
  for {
  i, ok := <-ch
  if !ok {
  break
  }
  fmt.Println("Received value:", i)
  }
}

func main() {
  myChannel := make(chan int)
  go runLoopSend(10, myChannel)
   go runLoopReceive(myChannel)
  time.Sleep(2 * time.Second)
}

수정

func runLoopReceive(ch chan int) {
  for i := range ch{
  fmt.Println("Received value:", i)
  }
}

◆ 버퍼 채널 - 비동기 채널

버퍼(buffered) 채널은 여러 값을 버퍼에 저장하는 특수한 채널

일반 채널과 다르게 버퍼 채널은 다음의 경우에만 대기

      ● 버퍼가 비어있는 채널이 데이터를 기다리는 경우

      ● 버퍼에 남은 공간이 없는 채널에 데이터를 보내는 경우

▶ 선언

make(chan 자료형, 개수)
package main

import (
  "fmt"
  "runtime"
)

func main() {
  runtime.GOMAXPROCS(1)
  done := make(chan bool, 2) // 버퍼가 2개인 버퍼 채널 생성
  count := 10 // 반복할 횟수

  go func() {
  for i := 0; i < count; i++ {
  done <- true // 채널에 true를 보냄
  fmt.Println("고루틴 : ", i) // 반복문의 변수 출력
  }
  }()

  for i := 0; i < count; i++ {
  <-done // 버퍼에 값이 없으면 대기, 값을 꺼냄
  fmt.Println("메인 함수 : ", i) // 반복문의 변수 출력
  }
}

◆ Select

select 구문을 사용해 여러 채널을 동시에 제어할 수 있음

select 사용해서 여러 채널에 데이터를 보내거나 받을 수 있고 먼저 활성화된 채널의 코드를 실행할 수 있음

select {
  case i := <-ch:
  fmt.Println("Received value:", i)
  case <-time.After(1 * time.Second):
  fmt.Printin("timed out") }

    ● select 구문은 2개의 채널을 사용

    ● 첫 번째 채널은 데이터를 수신하는 ch 채널이고 두 번째 채널은 time.After() 함수인데 이 함수는 select 구문에서 많이 사용되는 함수로 설정한 시간이 지나면 데이터가 도착하며 이 시간 동안 채널을 대기 시킴

    ● 채널에서 데이터를 송수신 시 타임아웃(timeout)을 설정할 때 주로 time.After() 함수를 사용

package main

import (
  "fmt"
  "time"
)

func main() {
  c1 := make(chan int)    // int형 채널 생성
  c2 := make(chan string) // string 채널 생성

  go func() {
  for {
  c1 <- 10                           // 채널 c1에 10을 보낸 뒤
  time.Sleep(100 * time.Millisecond) // 100 밀리초 대기
  }
  }()

  go func() {
  for {
  c2 <- "두번째 채널"              // 채널 c2에 Hello, world!를 보낸 뒤
  time.Sleep(500 * time.Millisecond) // 500 밀리초 대기
  }
  }()

  go func() {
  for {
  select {
  case i := <-c1:                // 채널 c1에 값이 들어왔다면 값을 꺼내서 i에 대입
  fmt.Println("c1 :", i) // i 값을 출력
  case s := <-c2:                // 채널 c2에 값이 들어왔다면 값을 꺼내서 s에 대입
  fmt.Println("c2 :", s) // s 값을 출력
  }
  }
  }()
  time.Sleep(10 * time.Second) // 10초 동안 프로그램 실행
}

◆ 동기화 객체

반드시 잠금이 필요하다면 sync 패키지를 사용

고루틴 간의 소통 방법은 채널이 적합하지만 잠금이나 뮤텍스(mutex, mutual exclusion object) 사용해야 하는 경우가 있는데 예를 들어 Go 언어의 표준 패키지인 http 패키지에서 http 서버 객체의 리스너를 제어하는 데 뮤텍스를 사용하는데 리스너는 여러 고루틴에서 접근할 수 있으므로 뮤텍스가 적절

컴퓨터 프로그래밍에서 뮤텍스는 자원(예를 들어 공유 메모리)에 여러 스레드의 접근을 제어하는 객체를 의미하는데 뮤텍스는 한 번에 한 개의 스레드만 데이터에 접근을 허용하기 때문에 붙여진 이름

소프트웨어에서 뮤텍스의 워크플로우는 일반적으로 다음과 같음

     ● 특정 스레드가 뮤텍스를 소유

     ● 다른 스레드는 뮤텍스를 소유할 수 없음

     ● 뮤텍스를 소유한 스레드는 마음대로 자원에 접근할 수 있음

     ● 작업이 끝나면 뮤텍스를 해제하고 다른 스레드는 뮤텍스를 소유하고자 서로 경쟁

고루틴은 완전한 스레드가 아니므로 뮤텍스를 사용해 여러 고루틴의 동일한 자원에 접근을 제어할 수 있음

동기화 객체 종류

     ● Mutex: 여러 스레드(고루틴)에서 공유되는 데이터를 보호할 때 주로 사용

     ● RWMutex: 읽기/쓰기 뮤텍스로 읽기와 쓰기 동작을 나누어서 잠금()을 걸 수 있음

     ● Cond: 조건 변수(condition variable)로 대기하고 있는 하나의 객체를 깨울 수도 있고 여러 개를 동시에 깨울 수도 있음

     ● Once: 특정 함수를 딱 한 번만 실행할 때 사용

     ● Pool: 멀티 스레드(고루틴)에서 사용할 수 있는 객체 풀로 자주 사용하는 객체를 풀에 보관했다가 다시 사용

     ● WaitGroup: 고루틴이 모두 끝날 때까지 기다리는 기능

     ● Atomic: 원자적 연산이라고도 하며 더 이상 쪼갤 수 없는 연산이라는 의미로 멀티 스레드(고루틴), 멀티코어 환경에서 안전하게 값을 연산하는 기능

▶ 뮤텍스 구조체

    ● sync.Mutex

    ● func (m *Mutex) Lock(): 뮤텍스 잠금

    ● func (m *Mutex) UnlockO: 뮤텍스 잠금 해제

▶ 뮤텍스를 사용하지 않은 경우

package main

import (
  "fmt"
  "runtime"
  "time"
)

func main() {
  runtime.GOMAXPROCS(runtime.NumCPU()) // 모든 CPU 사용

  var data = []int{} // int형 슬라이스 생성

  go func() {                             // 고루틴에서
  for i := 0; i < 1000; i++ {     // 1000번 반복하면서
  data = append(data, 1)  // data 슬라이스에 1을 추가

  runtime.Gosched()       // 다른 고루틴이 CPU를 사용할 수 있도록 양보
  }
  }()

  go func() {                             // 고루틴에서
  for i := 0; i < 1000; i++ {     // 1000번 반복하면서
  data = append(data, 1)  // data 슬라이스에 1을 추가

  runtime.Gosched()       // 다른 고루틴이 CPU를 사용할 수 있도록 양보
  }
  }()

  time.Sleep(2 * time.Second)      // 2초 대기

  fmt.Println(len(data))           // data 슬라이스의 길이 출력
}

▶ 뮤텍스를 사용할때

package main

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

func main() {
  runtime.GOMAXPROCS(runtime.NumCPU()) // 모든 CPU 사용

  var data = []int{}
  var mutex = new(sync.Mutex)

  go func() {                             // 고루틴에서
  for i := 0; i < 1000; i++ {     // 1000번 반복하면서
  mutex.Lock()            // 뮤텍스 잠금, data 슬라이스 보호 시작
  data = append(data, 1)  // data 슬라이스에 1을 추가
  mutex.Unlock()          // 뮤텍스 잠금 해제, data 슬라이스 보호 종료

  runtime.Gosched()       // 다른 고루틴이 CPU를 사용할 수 있도록 양보
  }
  }()

  go func() {                             // 고루틴에서
  for i := 0; i < 1000; i++ {     // 1000번 반복하면서
  mutex.Lock()            // 뮤텍스 잠금, data 슬라이스 보호 시작
  data = append(data, 1)  // data 슬라이스에 1을 추가
  mutex.Unlock()          // 뮤텍스 잠금 해제, data 슬라이스 보호 종료

  runtime.Gosched()       // 다른 고루틴이 CPU를 사용할 수 있도록 양보
  }
  }()

  time.Sleep(2 * time.Second) // 2초 대기

  fmt.Println(len(data)) // data 슬라이스의 길이 출력
}

▶ 읽기 쓰기 뮤텍스

     ● sync.RWMutex

     ● func (rw *RWMutex) Lock(), func (rw *RWMutex) Unlock(): 쓰기 뮤텍스 잠금, 잠금 해제

     ● func (rw *RWMutex) RLock(), func (rw *RWMutex) RUnlock(): 읽기 뮤텍스 잠금 및 잠금 해제

▶ 읽기 쓰기 뮤텍스 사용하지 않는 경우

package main

import (
  "fmt"
  "runtime"
  "time"
)

func main() {
  runtime.GOMAXPROCS(runtime.NumCPU()) // 모든 CPU 사용

  var data int = 0

  go func() {                                       // 값을 쓰는 고루틴
  for i := 0; i < 3; i++ {
  data += 1                         // data에 값 쓰기
  fmt.Println("write   : ", data)   // data 값을 출력
  time.Sleep(10 * time.Millisecond) // 10 밀리초 대기
  }
  }()

  go func() {                                       // 값을 읽는 고루틴
  for i := 0; i < 3; i++ {
  fmt.Println("read 1 : ", data)    // data 값을 출력(읽기)
  time.Sleep(1 * time.Second)       // 1초 대기
  }
  }()

  go func() {                                       // 값을 읽는 고루틴
  for i := 0; i < 3; i++ {
  fmt.Println("read 2 : ", data)    // data 값을 출력(읽기)
  time.Sleep(2 * time.Second)       // 2초 대기
  }
  }()

  time.Sleep(10 * time.Second)              // 10초 동안 프로그램 실행
}

 

▶ 읽기 쓰기 뮤텍스 사용하는 경우

package main

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

func main() {
  runtime.GOMAXPROCS(runtime.NumCPU()) // 모든 CPU 사용

  var data int = 0
  var rwMutex = new(sync.RWMutex) // 읽기, 쓰기 뮤텍스 생성

  go func() {                                       // 값을 쓰는 고루틴
  for i := 0; i < 3; i++ {
  rwMutex.Lock()                    // 쓰기 뮤텍스 잠금, 쓰기 보호 시작
  data += 1                         // data에 값 쓰기
  fmt.Println("write  : ", data)    // data 값을 출력
  time.Sleep(10 * time.Millisecond) // 10 밀리초 대기
  rwMutex.Unlock()                  // 쓰기 뮤텍스 잠금 해제, 쓰기 보호 종료
  }
  }()

다음장에서는 조건변수 구조체로 시작해서 계속해서 알아보는 시간을 가져보자

https://joylucky7.tistory.com/35

 

Go언어의 조건변수, Once 사용법, Pool 구조체 3장

앞서서 채널과 Select구문 그리고 Sync 를 보지 않았다면 아래 포스팅을 숙지후에 3장을 보도록 하자 https://joylucky7.tistory.com/34 Go언어의 채널(Channel), Select 구문, Sync 패키지 2장 동시성과 고루틴, 데

joylucky7.tistory.com

 

반응형

댓글