본문 바로가기

Developer/Go

Golang 고루틴(goroutine) 라이프사이클 관리 - context.Context

개요 (Overview)

Golang에서는 고루틴이라는 비동기 작업을 생성할 수 있고 쓰레드와 비교해 가볍기 때문에 동시에 수백 개를 만들어도 메모리를 많이 사용하지 않는다. 다만 고루틴은 비동기로 실행되고 쓰레드와 다르게 종료할 수 있는 함수를 제공하지 않는다. 즉 고루틴을 생성한 함수가 종료되더라도 생성된 고루틴은 남아서 계속 실행된다. 따라서 별도의 시그널을 통해서 종료해줘야 한다.

 

잠깐 실행되는 애플리케이션이면 큰 문제가 없지만 서버 애플리케이션과 같이 한번 실행하면 짧게는 며칠 길게는 몇 달을 넘게 계속 돌린다. 이때 생성한 고루틴을 제때 정리해주지 않으면 실행하는 고루틴이 계속 쌓여서 Memory Leak이 발생하게 되고 아무리 가벼운 고루틴이더라도 계속 쌓이면 결국 OOM(Out of Memory)가 발생할 수 있다. 서버의 가용성에 문제가 발생할 수 있고 잘못되면 서비스가 일시적으로 중단될 가능성이 있다.

 

따라서 생성한 고루틴이 더 이상 사용할 필요가 없어지면 꼭 정리를 해줘야 하는데 이때 사용할 수 있는 방법은 여러 가지가 있지만 여기서는 Golang에서 제공하는 Context와 Channel을 이용해보려고 한다.

 

컨텍스트 (Context)

단순 번역으로는 `맥락`을 의미한다.

 

 

context package - context - Go Packages

Discover Packages Standard library context Version: go1.22.2 Opens a new window with list of versions in this module. Published: Apr 3, 2024 License: BSD-3-Clause Opens a new window with license information. Imports: 5 Opens a new window with list of impor

pkg.go.dev

 

Golang에서 컨텍스트는 작업을 수행할 때 데이터를 전달하고 조건에 따라 흐름을 제어하는 역할을 해주는 패키지이다.

기본적으로 생성하는 컨텍스트는 Background, TODO가 있으며 특정 데이터를 전달할 때는 WithValue를 이용해서 컨텍스트를 생성할 수 있다. 이렇게 생성된 컨텍스트는 절대(never) 취소되지 않으며 데이터도 없고 만료 일시도 없다.

 

컨텍스트 생성

  • context.Background()
  • context.TODO()
  • context.WithValue(parent Context, key, val any) Context
    • 인자로 컨텍스트를 요구하기 때문에 위 Background, TODO 중 하나를 만들어서 인자로 넣어주면 된다.

컨텍스트 흐름 제어 추가

  • context.WithCancel(parent Context) Context
  • context.WithCancelCause(parent Context) Context
  • context.WithDeadline(parent Context, d time.Time) Context
  • context.WithDeadlineCause(parent Context, d time.Time, cause error) Context
  • context.WithTimeout(parent Context, timeout time.Duration) Context
  • context.WithTimeoutCause(parent Context, timeout time.Duration, cause error) Context

Canel은 취소 기능을 제공하여 필요할 때 취소하여 흐름을 제어할 수 있다.

Deadline과 Timeout은 컨텍스트에 대한 만료 시간을 제공하여 처리 시간에 대해 흐름 제어가 가능하며 Deadline은 지정된 일시(time.Duration)를 인자로 받으며 Timeout은 기간(time.Duration)을 인자로 받아 내부에서 다시 WithDeadline을 호출한다. 즉 WithDeadline은 개발자가 지정된 일시를 계산하지 않고 특정 기간만 지정할 수 있는 편의성을 제공하는 함수다.

 

Cause는 취소될 때 error 타입의 인자를 같이 받아서 왜 취소되었는지 알 수 있도록 제공하는 함수다.

WithCancelCause 함수의 경우 컨텍스트를 생성할 때는 error 인자를 받지 않고 취소할 때 error 인자를 받아서 취소 사유를 알 수 있으며 그 외에는 생성 시 인자를 받아서 사유를 알 수 있도록 되어 있다.

 

사용 방법 (How to use)

함수 처리 중 특정 사유(인터럽트, 에러 등)로 인한 고루틴 흐름 제어 (컨텍스트 취소)

  1. context.Background() 또는 TODO()로 빈 컨텍스트 생성
  2. 생성된 빈 컨텍스트를 이용해 WithCancel()로 취소 가능한 컨텍스트를 생성
  3. 생성된 컨텍스트를 포함하여 고루틴 생성
  4. 작업 (메인 함수는 고루틴 완료 대기)
  5. 인터럽트 또는 메인 함수에서 에러가 발생하여 취소(Cancel) 함수 호출
  6. 취소 함수 호출 후 실행 중인 고루틴이 모두 종료될 때까지 대기
  7. 모두 종료 후 메인 함수 정리 후 종료

 

샘플 코드 (Sample code)

별도의 기능을 사용하지 않으면 생성한 고루틴의 상태를 알 수 없기 때문에 sync.WaitGroup을 이용해 생성한 고루틴의 상태를 간접적으로 알 수 있도록 하였다.

 

 

meaningful-go/basic-goroutine-manage at main · torrang/meaningful-go

Go언어 사용 시 유용한 코드나 패턴을 작성하여 사용하기 위한 프로젝트. Contribute to torrang/meaningful-go development by creating an account on GitHub.

github.com

package main

import (
	"context"
	"log"
	"sync"
	"time"
)

// user defined asynchronous task
func async_task(wg *sync.WaitGroup, ctx context.Context, i int) {
	for {
		select {
		case <-ctx.Done():
			// using break does not escape for-loop
			// should use return instead or flag
			log.Printf("terminate async task %d", i)
			wg.Done()
			return
		default:
			log.Printf("async task %d is running", i)
			time.Sleep(time.Duration(i) * time.Second)
		}
	}
}

func main() {
	log.Printf("start basic-goroutine-manage")
	waitGroup := sync.WaitGroup{}
	ctx, cancelFn := context.WithCancel(context.Background())

	// run goroutines
	for i := 1; i <= 3; i++ {
		waitGroup.Add(1)
		go async_task(&waitGroup, ctx, i)
	}

	// cancel goroutines after 10 seconds
	go func() {
		log.Printf("wait for 10 seconds")
		time.Sleep(time.Duration(10) * time.Second)
		log.Printf("time is over, cancel goroutines")
		cancelFn()
	}()

	// wait goroutines
	log.Printf("wait goroutines")
	waitGroup.Wait()

	log.Printf("all goroutines are terminated")
	log.Printf("terminate basic-goroutine-manage")
}