TIL 05 - 고루틴(Goroutine)과 채널(Channel)(Golang)

프동프동·2023년 2월 10일
0

TIL

목록 보기
5/46
post-thumbnail

고루틴

동시성을 구현할 때 다른 언어의 스레드 등의 방식과는 차별화된 방법

  • 특징
    • 독립적으로 실행, 동시에 실행되는 내장함수
    • 동일한 주소 공간에서 다른 고루틴과 동시에 실행 가능
    • 가볍지만 스택 공간 할당보다 비용이 다소 큼(스택 지속 증가 후 힙 영역 할당)
    • 타 언어의 사용하는 쓰레드(Thread)와 비슷한 기능
      • os쓰레드를 이용하는 경량 쓰레드(light weight thread)
        • 수 많은 고루틴 동시 생성 가능
        • 고루틴을 많이 생성해도 os단에서 컨텍스트 스위칭 비용이 발생하지 않는다
          • 고루틴 자체에서 컨텍스트 스위칭이 되므로 컨텍스트 스위칭이 없는건 아니다.
    • 비동기적 함수 루틴 실행(매우 적은 용량 차지)
    • 응답성 향상
    • 자원 공유를 효율적으로 활용 가능
    • 작업이 분리되어 코드 간결해짐
  • 사용 방법
    • go 키워드 사용, 완료 후 고루틴 자동 종료
    • 백그라운드 명령을 실행하기 위한 Unix 셀의 & 표기법과 유사
      • ex. ./ready & - 백그라운드 데몬 실행
    • 고루틴 대기 활용
      • WaitGroup : WaitGroup 선언
        wg := new(sync.WaitGroup)
      • Add(1) : WaitGroup에 고루틴 1개 추가
        wg.Add(1)
      • Done() : 하나의 고루틴이 종료됨을 알림
        wg.Done()
      • Wait() : WaitGroup내 모든 고루틴이 종료될 때 까지 대기
        wg.Wait()
      • 예제
        func main() {
        	wg := new(sync.WaitGroup)
        
        	for i := 0; i < 100; i++ {
        		// 고루틴 추가
        		wg.Add(1)
        		go func(n int) {
        			fmt.Println("WaitGroup : ", n)
        			wg.Done()
        		}(i)
        	}
        	// Add로 입력한 고루틴의 수와 == Done() 호출한 횟수가 같아야 한다.
        	// 끝날때까지 대기
        	wg.Wait()
        	fmt.Println("WaitGroup End!")
        }
  • 주의
    • 싱글 루틴에 비해 항상 빠른 처리 결과를 갖는 것은 아니다.
    • 구현이 어려우며 테스트 및 디버깅이 어려움
    • 전체 프로세스의 사이드 이펙트가 나올 수 있다.
    • 성능 저하 발생 우려
    • 데드락
      • 동일한 메모리 자원을 여러 고루틴에서 접근할 때 동시성 문제가 발생
      • 해결 방법 (mutex, atomic) 이용
        var cnt int64 = 0
        
        	wg := new(sync.WaitGroup)
        
        	for i := 0; i < 5000; i++ {
        		wg.Add(1)
        		go func(n int) {
        			cnt += 1
        	 		// atomic.AddInt64(&cnt, 1)
        			wg.Done()
        		}(i)
        	}
        	for i := 0; i < 2000; i++ {
        		wg.Add(1)
        		go func(n int) {
        			cnt -= 1
        			// atomic.AddInt64(&cnt, -1)
        			wg.Done()
        		}(i)
        	}
        
        	// 끝날때까지 대기
        	wg.Wait()
        	fmt.Println("WaitGroup End! >>>>> ", cnt)
        	// finalCnt := atomic.LoadInt64(&cnt)
        	// fmt.Println("WaitGroup End! >>>>> ", finalCnt)
      • mutex의 문제점으로는 과도한 락킹으로 성능이 하락하기도 한다. 또한 락을 획득하고 반납하는데도 성능을 먹는다. 작은 범위에서 확실할 때만 사용하는 것이 좋다.
      • 다른 해결방법으로는 영역을 나누거나, 역할을 나누는 방법이있다.

채널(Channel)

채널(Channel)

고루틴이라는 동시에 실행되는 함수들 사이에서 데이터를 주고 받거나 흐름제어를 하기 위해서 채널을 사용

  • 특징
    • 고루틴간 리소스 또는 데이터를 공유하는데 사용됨
    • 고루틴간 파이프 역할을 하여 동기 교환을 보장
    • 주어진 시간에 하나의 고루틴만 데이터 항목에 엑세스할 수 있으므로 설계상 데이터 경쟁이 발생할 수 없다
  • 사용 방법
    • make(chan 자료형)
      • 내장형, 명명형, 구조체형, 참조형의 값과 포인터를 공유 가능
    • “←” 채널 호출 표현
      ch := make(chan int) // int형 값을 전달할 수 있는 채널 생성
      
      // ...
      ch <- 1 // 1을 채널로 전송
      
      var temp int = <-ch // 채널로 받은 값은 수신
    • 버퍼되지 않는 채널, 버퍼된 채널 두가지 유형 존재
      • make(chan 자료형, 버퍼 수)
        • 버퍼 수를 1개 이상 설정하면 버퍼된 채널이 생성
      • 버퍼되지 않는 채널 : 동기 통신을 수행, 송수신 발생 즉시 교환 보장
      • 버퍼된 채널 : 비동기 통신을 수행, 즉시 교환 보장 안됨
    • 채널을 매개변수로 받는 함수를 실행하기 위해서는 고루틴으로 만들어야 한다.
      func work1(v chan int) {
      	time.Sleep(1 * time.Second)
      	v <- 1 // 1을 채널로 전송(송신)
      }
      
      func main() {
      	v := make(chan int) // int형 채널 선언
      	go work1(v)
      	fmt.Println(<-v)
      	// time.Sleep() 를 사용할 필요가 없다.
      }

Select 문

select 문은 switch와 유사한 문법 사용, case에 채널이 사용된다는 점이 차이

  • 주의
    • default문 주의
      • case문에 챈러이 준비되지 않았더라도 대기하지 않고 default 문을 실행한다.
  • 사용 방법
    	ch1 := make(chan int)
    	ch2 := make(chan string)
    
    	go func() {
    		for {
    			ch1 <- 77
    			time.Sleep(250 * time.Millisecond)
    		}
    	}()
    	go func() {
    		for {
    			ch2 <- "Golang H!i"
    			time.Sleep(500 * time.Millisecond)
    		}
    	}()
    
    	go func() {
    		for {
    			select {
    			case num := <-ch1:
    				fmt.Println("ch1: ", num)
    			case str := <-ch2:
    				fmt.Println("ch2: ", str)
    		  //default:
    			// 
    			}
    
    		}
    	}()
profile
좋은 개발자가 되고싶은

0개의 댓글