『Go in Action』의 내용을 기반으로 학습 및 정리했습니다.
동시성(Concurrency)
Go에서 동시성은 여러 작업을 독립적으로 실행하고 관리하는 능력을 의미한다. Go는 goroutine이라는 경량 스레드를 제공하며, 런타임 스케줄러가 이를 효율적으로 관리한다.
2026.05.24 - [Go] - Go의 특징과 핵심 개념 정리 — goroutine, interface, channel 이해하기
고루틴 간의 동기화 및 데이터 전달을 위해 채널(Channel)이라는 핵심 기능을 제공한다.
프로세스(Process)와 스레드(Thread)
프로세스는 실행 중인 프로그램의 인스턴스이며, 최소 하나 이상의 스레드를 가진다.
스레드는 운영체제가 관리하는 실행 단위로, 실제로 CPU에서 명령어를 실행하는 경로이다.
Go 런타임은 자체 스케줄러를 사용하여 수많은 고루틴을 적은 수의 운영체제 스레드에 효율적으로 분배한다.
Go 스케줄러의 구성 요소
Go 스케줄러는 일반적으로 다음 세 가지 요소로 설명된다.
- G(Goroutine)
- M(Machine, 운영체제 스레드)
- P(Processor, 논리 프로세서)
여기서 P는 CPU 자체가 아니라 Go 런타임이 사용하는 실행 컨텍스트이다.
고루틴이 생성되면 실행 대기 상태가 되며 스케줄러의 실행 큐에 등록된다.
스케줄러는 다음 두 종류의 큐를 사용한다.
- Global Run Queue
- Local Run Queue
새로운 고루틴은 일반적으로 실행 큐에 추가되고, 논리 프로세서(P)는 자신의 Local Run Queue에 있는 고루틴을 우선 실행한다. 작업이 부족하면 Global Run Queue나 다른 P의 큐에서 작업을 가져오는 Work Stealing 기법을 사용한다.
시스템 콜과 스케줄링
고루틴이 파일 읽기와 같은 블로킹 시스템 콜을 수행하면 해당 작업을 실행 중인 운영체제 스레드(M)가 블로킹될 수 있다.
이 경우 Go 스케줄러는 P를 기존 스레드에서 분리하고, 다른 스레드에 연결하여 다른 고루틴들이 계속 실행될 수 있도록 한다.
네트워크 I/O와 Network Poller
Go는 네트워크 I/O에 대해 특별한 최적화를 제공한다.
예를 들어 다음과 같은 작업이 있다.
- HTTP 요청 대기
- TCP 소켓 데이터 수신 대기
- 데이터베이스 네트워크 응답 대기
이러한 작업은 대부분 CPU를 사용하지 않고 응답만 기다리는 상태이다.
Go 런타임은 Network Poller를 통해 이러한 대기 작업을 관리한다.
고루틴은 네트워크 응답을 기다리는 동안 실행 큐에서 제외되고, 응답이 도착하면 다시 실행 큐에 등록되어 작업을 이어서 수행한다.
덕분에 적은 수의 스레드로도 수많은 네트워크 연결을 처리할 수 있다.
병렬성(Parallelism)
병렬성은 여러 작업이 실제로 동시에 실행되는 것을 의미한다.
동시성과 병렬성은 서로 다른 개념이다.
- 동시성: 여러 작업을 효율적으로 관리하는 것
- 병렬성: 여러 작업이 물리적으로 동시에 실행되는 것
고루틴을 병렬적으로 실행하려면 둘 이상의 논리 프로세서(P)가 필요하다.
또한 진정한 병렬성을 얻으려면 여러 CPU 코어를 가진 환경에서 프로그램이 실행되어야 한다.
단일 CPU 코어 환경에서는 여러 고루틴이 존재하더라도 실제로는 번갈아 실행될 뿐이다.
고루틴(Goroutine)
고루틴은 Go 런타임이 관리하는 경량 실행 단위이다.
새로운 고루틴은 go 키워드로 생성할 수 있다.
go func() {
fmt.Println("goroutine")
}()
고루틴은 매우 적은 메모리를 사용하며 수천 개 이상 생성할 수 있다.
WaitGroup
sync.WaitGroup은 여러 고루틴의 작업 완료를 기다리기 위한 동기화 도구이다.
내부적으로 카운팅 세마포어(Counting Semaphore) 방식으로 동작한다.
세마포어란 특정 자원에 접근 가능한 개수를 숫자로 관리하는 동기화 기법이다.
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for count := 0; count < 3; count++ {
fmt.Println("1:", count)
}
}()
go func() {
defer wg.Done()
for count := 0; count < 3; count++ {
fmt.Println("2:", count)
}
}()
wg.Wait()
Add(2)는 완료를 기다릴 작업이 2개임을 의미한다.
각 고루틴은 작업이 끝나면 Done()을 호출하여 카운터를 감소시킨다.
Wait()는 카운터가 0이 될 때까지 현재 고루틴을 대기시킨다.
defer
defer는 현재 함수가 종료되기 직전에 특정 함수를 실행하도록 예약하는 키워드이다.
따라서 위 코드에서는 고루틴이 정상 종료되거나 중간에 return 되더라도 Done() 호출이 보장된다.
Race Condition (경쟁 상태)
두 개 이상의 고루틴이 동기화 없이 동일한 공유 자원에 동시에 접근하면 경쟁 상태가 발생할 수 있다.
counter++
위 코드는 단순해 보이지만 실제로는
- 값 읽기
- 값 증가
- 값 저장
이라는 여러 단계로 구성된다.
만약 여러 고루틴이 동시에 실행하면 예상하지 못한 결과가 발생할 수 있다.
이를 방지하기 위해서는 공유 자원에 대한 접근이 원자성(Atomicity)을 보장해야 한다.
공유 자원 잠금
Go는 여러 고루틴이 안전하게 공유 자원에 접근할 수 있도록 다양한 동기화 도구를 제공한다.
- sync 패키지
- sync/atomic 패키지
Mutex
뮤텍스(Mutex)는 특정 코드 영역을 한 번에 하나의 고루틴만 실행할 수 있도록 보장한다.
이 영역을 임계 구역(Critical Section)이라고 한다.
import "sync"
var mutex sync.Mutex
mutex.Lock()
counter++
mutex.Unlock()
Lock()으로 잠금을 획득한 고루틴만 임계 구역에 진입할 수 있다.
다른 고루틴은 Unlock()이 호출될 때까지 대기한다.
채널(Channel)
버퍼가 없는 채널
ch := make(chan int)
버퍼가 없는 채널은 송신자와 수신자가 동시에 준비되어 있어야 한다.
ch <- 10
위 코드는 다른 고루틴이 값을 받을 때까지 대기한다.
즉 송신과 수신이 반드시 동기화된다.
버퍼가 있는 채널
ch := make(chan int, 3)
버퍼가 있는 채널은 지정한 개수만큼 값을 저장할 수 있다.
ch <- 1
ch <- 2
ch <- 3
버퍼가 가득 차지 않았다면 수신자가 없어도 송신이 가능하다.
따라서 버퍼가 없는 채널과 달리 송신과 수신이 동시에 발생할 필요는 없다.
버퍼가 모두 차면 송신자는 대기하게 되며, 수신자가 값을 꺼내면 다시 송신할 수 있다.
'Programming > Go' 카테고리의 다른 글
| Go의 동시성 패턴 (0) | 2026.06.14 |
|---|---|
| Go의 타입 시스템 (0) | 2026.06.07 |
| Package와 Go의 데이터 컬렉션 (0) | 2026.05.31 |
| Go의 특징과 핵심 살펴보기 (0) | 2026.05.24 |
