본문 바로가기

golang

golang10 채널과 context

채널

  • 고루틴끼리 데이터를 송수신하기 위한 통로 역할
  • 채널을 정의한 타입으로만 송수신 가능
  • FIFO방식으로 데이터를 전달
    • 먼저 송신된 데이터먼저 받는다
  • 데이터 송수신 시 송신측과 수신측은 송수신이 완료될 때까지 대기한다.
    • deadlock발생 가능
func TestGoroutine(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(2)
	myChan := make(chan int)
	go func() {
		defer wg.Done()
		myChan <- 100
		value1 := <-myChan
		fmt.Println(value1)
	}()
	go func() {
		defer wg.Done()
		value2 := <-myChan
		myChan <- value2 + 100
		fmt.Println(value2)
	}()
	wg.Wait()
}

myChan이라는 int타입의 채널을 생성해서 고루틴끼리 주고받는다.

  • 채널생성
    • channel:=make(chan int)
  • 채널을 통해 데이터 송신
    • myChan <- 100
  • 채널을 통해 데이터를 수신
    • value2 := <-myChan

deadlock

  • 정의: 서로다른 두 프로세스가 자신의 자원을 점유하고있는 상태에서 상대방이 점유중인 자원을 요청하는 상태
  • deadlock은 4가지 조건을 모두 만족해야 발생한다.
    • Mutual Exclusion: 임의의 순간에 프로세스는 반드시 자원을 점유중이어야 한다.
    • Hold and Wait: 자원을 점유중인 상태에서 다른 프로세스의 자원요청을 대기한다.
    • No Preemption: 프로세스가 직접 자원을 반납하기 전까지 다른 프로세스로가 점유중인 자원을 뺏을 수 없다.
    • Circular Wait: 자원을 점유하고 다른 프로세스의 자원을 요청하는 상태가 마치 Circular처럼 이루어져야 한다.

Channel의 Deadlock

  • case1: 채널로 데이터를 송신한 상태에서 프로세스가 종료
func TestGoroutine(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)
	myChan := make(chan int)
	go func() {
		defer wg.Done()
		myChan <- 100 // deadlock!!
	}()
	wg.Wait()
}
  • case2: 채널을 통해 데이터 수신을 영원히 대기
func TestGoroutine(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)
	myChan := make(chan int)
	go func() {
		defer wg.Done()
		value := <-myChan // deadlock!!
		fmt.Println(value)
	}()
	wg.Wait()
}

위의 코드를 돌려보면 프로그램이 무한히 대기하는 현상이 발생하지 않고 deadlock이 발생하였다는 에러를 반환한다. 즉 golang은 deadlock detection기능을 지원한다는 뜻이다.

golang의 deadlock detection은 go로 만든 프로세스 내부에서만 탐지되며 OS상에서 발생하는 deadlock은 탐지할 수 없다.

채널의 버퍼

  • 채널도 버퍼가 존재하여 송신중인 값을 잠시 저장할 수 있다.
    • 왜 필요한가? => deadlock방지
    • 수신측이 받을때까지 송신측이 무한정 대기하는 상태를 방지
  • default: 0
  • 버퍼가 100인 채널 생성 === 송신할 데이터 100개를 버퍼에 넣어놓고 다른일을 할 수 있다.
    • channel:=make(chan int,100)
  • 버퍼를 놓는다고 해서 deadlock에 완전히 벗어나지 않는다.
    1. 수신측에서 발생하는 deadlock은 해결하지 못한다
    2. 100개의 데이터를 버퍼에 저장된 상태에서 101번째의 데이터를 송신할 때는 송신측에서 버퍼에 삽입되기 전까지 대기한다.

select

  • 고루틴의 switch기능
  • 왜 필요한가?
    • 여러개의 채널을 사용할 때 먼저 응답받는 채널을 먼저 처리가 가능하다
func TestGoroutine(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)
	chan1 := make(chan int)
	chan2 := make(chan int)
	chan3 := make(chan int)
	go func() {
		defer wg.Done()
		select {
		case retChan1 := <-chan1:
			fmt.Println(retChan1)
		case retChan2 := <-chan2:
			fmt.Println(retChan2)
		case retChan3 := <-chan3:
			fmt.Println(retChan3)
		}
	}()
	chan2 <- 100
	wg.Wait()
}

채널들을 case로 지정하고 응답이 오는 채널을 처리해준다. 채널에서 값이 올 때까지 select에서 block된다. block이 아닌 처리과정이 더 필요하다면 default를 사용하면 된다.

func TestGoroutine(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)
	chan1 := make(chan int)
	chan2 := make(chan int)
	chan3 := make(chan int)
	go func() {
		defer wg.Done()
		select {
		case retChan1 := <-chan1:
			fmt.Println(retChan1)
		case retChan2 := <-chan2:
			fmt.Println(retChan2)
		case retChan3 := <-chan3:
			fmt.Println(retChan3)
		default:
			fmt.Println("not found")
		}
	}()
	
	wg.Wait()
}

채널들이 모두 값을 수신하기위해 대기중인 상태이므로 default를 실행한다.


context

  • 여러개의 고루틴을 제어할 수 있는 golang의 내장객체
  • context객체들은 트리구조로 구성된다.
    • root: background

작업 취소 context

  • 개발자가 원하는 타이밍에 고루틴을 제어시키는 기능
  • ctx, cancelFunc:= context.WithCancel(parentContext)
    • ctx: 작업 취소 context
    • cancelFunc: 작업 취소 명령을 요청하는 함수
    • parentContext: 부모 context
func TestGoroutine(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)
	ctx, cancelFunc := context.WithCancel(context.Background())
	go func() {
		defer wg.Done()
		tick := time.Tick(1 * time.Second)
		// 작업취소를 수행하기 전까지 1초마다 tick...을 출력
		for {
			select {
			// 작업 취소 명령이 떨어지면 함수를 종료시킴
			case <-ctx.Done():
				return
			case <-tick:
				fmt.Println("tick...")
			}
		}
	}()
	// 5초후 작업취소 명령을 요청
	time.Sleep(5 * time.Second)
	cancelFunc()
	wg.Wait()
}

제한시간을 거는 context

  • context를 통해 고루틴의 시간제한을 설정
    • 제한시간 내로 작업을 완료하지 못하면 고루틴에 개입이 가능
  • ctx, cancelFunc:= context.WithTimeout(parentContext,limit)
    • ctx: 작업 취소 context
    • cancelFunc: 작업 취소 명령을 요청하는 함수
    • parentContext: 부모 context
    • limit: 유효시간
func TestGoroutine(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)
	// 작업시간이 최대 5초인 context생성
	ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
	go func() {
		defer wg.Done()
		tick := time.Tick(1 * time.Second)
		for {
			// 5초동안 tick을 출력하다가 종료
			select {
			case <-ctx.Done():
				return
			case <-tick:
				fmt.Println("tick...")
			}
		}
	}()
	wg.Wait()
}

작업을 지정하는 context

  • context를 통해 특정 값을 전달
  • key-value형식으로 context내에 값을 삽입하여 이를 불러오는 방식으로 사용
  • ctx:= context.WithValue(parentContext,key,value)
    • ctx: 작업 취소 context
    • parentContext: 부모 context
    • key: context에 저장된 값을 불러오기위한 key
      • type: string
    • value: context내에 저장된 값
      • type: {}interface -> 모든 타입이 가능함
func TestGoroutine(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)
	ctx := context.WithValue(context.Background(), "myFunc", func() {
		fmt.Println("wahaha")
	})
	go func() {
		defer wg.Done()
		// myFunc값을 불러와서 타입assertion을 수행
		myFunc := ctx.Value("myFunc").(func())
		myFunc()
	}()
	wg.Wait()
}

context합치기

위의 context내용들을 보면 context를 생성하기 위해선 부모 context정보가 반드시 필요함을 알 수 있다. 이를 통해 여러개의 context를 묶어서 제어가 가능하다.

func TestGoroutine(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)
	// myFunc값을 삽입
	parentCTX := context.WithValue(context.Background(), "myFunc", func() {
		fmt.Println("wahaha")
	})
	// 제한시간을 5초로 설정
	ctx, _ := context.WithTimeout(parentCTX, 5*time.Second)
	go func() {
		defer wg.Done()
		tick := time.Tick(1 * time.Second)
		for {
			select {
			case <-ctx.Done():
				return
			case <-tick:
				// 값을 가져와서 함수를 실행함
				myFunc := ctx.Value("myFunc").(func())
				myFunc()
			}
		}
	}()
	wg.Wait()
}

제한시간이 5초동안 myFunc값을 불러와서 호출시키는 context

'golang' 카테고리의 다른 글

golang1.18릴리즈 노트: fuzzing, generic  (0) 2022.06.23
golang9 goroutine  (0) 2022.03.02
golang8 interface  (0) 2022.02.28
golang7 slice  (0) 2022.01.09
golang6 구조체  (0) 2021.12.26