언어 및 도구들 공부/Go

Go) GoRoutine, Mutex, Channel, waitGroup

Dr.Mlem 2025. 4. 13. 20:17

Q1.

일단 내가 이해한 내용이야.
1. 고루틴은 한 번에 한 함수를 GO 키워드를 붙여서 여러개 작동시키는 기능.

2. waitgroup은 waitgroup에 추가된 고루틴을 끝날때까지 다음 코드로 진행하지 않게 대기시켜주는 기능.
3. Mutex는 특정 요소에 동시에 접근할 때, 해당 요소에 실질적으로는 한 번에 한 번씩만 접근할 수 있게 제한을 거는 기능

 

이렇게 이해했고, 채널은 해당 코드만 보면 잘 모르겠어.
1. 내가 이해한 내용이 맞는지?
2. 채널을 설명하는 코드를 좀 더 작성해줄래?

 

 

 

A1.

네 정리 너무 잘했어!
고루틴, WaitGroup, Mutex에 대한 개념 이해는 거의 완벽해.
아래에 각각 코멘트 달아볼게:


✅ 1. 너의 이해 내용 검토

항목 너의 설명 정확도 보출 설명
고루틴 go 키워드를 붙여서 여러 개 함수 병렬 실행 ✅ 완전 정확 Go에서 병렬 작업을 위한 lightweight thread.
WaitGroup 고루틴이 끝날 때까지 대기시키는 기능 ✅ 아주 정확 wg.Add()로 고루틴 개수 설정, wg.Done()으로 종료 알림
Mutex 한 번에 하나만 자원 접근하게 만드는 기능 ✅ 정확 공유 자원 보호용. 여러 고루틴이 동시에 접근 시 충돌 방지

❓ 2. 채널이 잘 이해 안 되는 이유

채널은 단순히 “값을 보내고 받는다” 이상으로 동기화 역할도 함께 하거든.
즉, 채널은 데이터 통로이자, 고루틴 간 신호선이야.


✅ 채널의 핵심 개념 한 줄 요약

고루틴 간 데이터를 안전하게 전달하기 위한 양방향 통신 라인


✅ 쉽게 설명하는 비유

  • 고루틴 1: 편지를 써서 우체통(채널)에 넣는다
  • 고루틴 2: 편지가 올 때까지 우체통 앞에서 기다린다

채널은 그 우체통 역할을 하는 것


✅ 예제 1: 값 하나 주고받기

package main

import "fmt"

func main() {
	ch := make(chan string) // string 타입을 주고받는 채널

	go func() {
		ch <- "안녕, 고루틴!" // 채널로 메시지 전송
	}()

	msg := <-ch // 채널로부터 메시지 수신
	fmt.Println("받은 메시지:", msg)
}

🔍 이 예제에서 중요한 점:

  • go func() 안의 고루틴이 메시지를 채널에 보냄 (ch <-)
  • 메인 함수가 msg := <-ch 로 받을 때까지 서로 기다림

✅ 예제 2: 채널로 여러 개의 값 전달하기

package main

import "fmt"

func main() {
	ch := make(chan int)

	go func() {
		for i := 1; i <= 3; i++ {
			ch <- i // 채널로 값 전송
		}
		close(ch) // 채널 닫기 (수신 측에서 range 사용 가능하게)
	}()

	for val := range ch { // 채널에서 값 수신
		fmt.Println("받은 값:", val)
	}
}

🧠 여기서 배울 점:

  • close(ch)를 통해 더 이상 보낼 값 없음을 알려줌
  • 수신 측에서는 range ch로 편하게 반복 수신 가능

✅ 예제 3: 채널은 동기화 장치 역할도 한다

package main

import (
	"fmt"
	"time"
)

func main() {
	done := make(chan bool)

	go func() {
		fmt.Println("고루틴 작업 중...")
		time.Sleep(1 * time.Second)
		done <- true // 작업 완료됨을 알림
	}()

	<-done // 고루틴이 끝날 때까지 여기서 대기
	fmt.Println("작업 완료 후 main 계속 실행")
}

🧠 이건 WaitGroup 없이 채널을 이용해 종료를 기다리는 방식
채널은 동기화용 도구로도 사용 가능


✅ 정리 요약

개념 역할
고루틴 병렬로 작업을 실행하는 lightweight 쓰레드
WaitGroup 고루틴 종료 대기
Mutex 공유 자원에 대한 동시 접근 제어
Channel 고루틴 간 데이터 전달 + 동기화 수단

원하면 이제 unbuffered vs buffered 채널 차이,
또는 select 문을 사용하는 고급 채널 예제도 보여줄 수 있어.

지금 이 흐름에서 더 깊이 들어가고 싶은 포인트 있어? 😎

 

 

 

Q2.

잠시만, waitgroup 사용할 때

package main

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

func doTask(id int, wg *sync.WaitGroup) {
	defer wg.Done()
	time.Sleep(300 * time.Millisecond)
	fmt.Printf("[비동기] 작업 %d 완료\n", id)
}

func main() {
	var wg sync.WaitGroup
	start := time.Now()
	
	// ✅ 작업이 동시에 실행됨 (비동기)
	// 전체 작업 소요 시간은 약 300ms
	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go doTask(i, &wg)
	}

	wg.Wait()
	fmt.Println("총 소요 시간:", time.Since(start))
}

이렇게 사용하잖아?
코드만 보았을 때 wg를 어떻게 사용하라는 말인지 잘 모르겠어.
wg를 사용하는 방법을 알려줘.
필요한 파라미터나 고 루틴에 필요한 설정이라든가, 그냥 지금 코드를 보았을때 어떻게 사용되는지 하나도 모르겠어.

 

 

 

A2.

너무 좋은 질문이야.
지금처럼 코드만 보면 wg.Add(1) / wg.Wait() / wg.Done() 이 왔다갔다하는데,
딱 보면 뭘 왜 하는 건지 감이 안 올 수 있어.
이걸 하나하나 순서대로 아주 쉽게 설명해줄게.


✅ WaitGroup을 쓰는 이유

고루틴을 여러 개 띄우면 병렬로 작업이 실행돼.
그런데 main 함수는 고루틴이 끝날 때까지 기다려주지 않아.
그래서 고루틴이 도는 도중에 main()이 종료돼버리면,
작업이 완료되지 않고 프로그램이 종료됨.

➡ 이때 “고루틴이 다 끝날 때까지 기다려줘” 라고 알려주는 게 sync.WaitGroup


✅ WaitGroup 구조 이해

👇 구조를 비유로 설명하면:

개념 역할(비유)
wg.Add(1) "작업 한 개 추가됨!" 이라고 메모하기
go func(...){ defer wg.Done() }() 고루틴 하나 실행, 끝나면 메모 한 개 지움
wg.Wait() "메모된 작업이 전부 끝날 때까지 기다리기"

✅ 예제 코드 분석

var wg sync.WaitGroup       // 1️⃣ WaitGroup 변수 선언

for i := 1; i <= 3; i++ {
	wg.Add(1)               // 2️⃣ 고루틴 하나를 만들 거니까 작업 추가

	go doTask(i, &wg)       // 3️⃣ 고루틴 실행 & wg를 넘김
}

wg.Wait()                   // 6️⃣ 고루틴들이 Done 호출할 때까지 대기

📦 고루틴 안에서 호출되는 함수: doTask

func doTask(id int, wg *sync.WaitGroup) {
	defer wg.Done()         // 5️⃣ 작업이 끝나면 "완료!" 표시

	time.Sleep(300 * time.Millisecond)
	fmt.Printf("작업 %d 완료\n", id)
}

✅ 전체 흐름 요약 순서

  1. wg.Add(1) → "고루틴 하나 시작할 거야!" 라고 알려줌
  2. go doTask() → 실제 고루틴 실행
  3. 고루틴 내부에서 defer wg.Done() → "이 작업 끝났어!" 라고 표시
  4. main()은 wg.Wait()에서 모든 Done이 호출될 때까지 대기함

🔍 꼭 기억할 것

  • Add(n) → 얼마나 기다려야 할지를 설정
  • Done() → 고루틴이 끝났다고 알림
  • Wait() → 다 끝날 때까지 main 함수 멈춰 있음

💬 한 줄 요약

WaitGroup은 고루틴 수만큼 Add()하고, 각 고루틴 안에서 Done() 호출한 뒤, main()은 Wait()로 기다리는 동기화 도구야.


📌 필수 형태 템플릿

var wg sync.WaitGroup

for i := 0; i < N; i++ {
	wg.Add(1)

	go func() {
		defer wg.Done()
		// 작업 수행
	}()
}

wg.Wait() // 여기서 모든 고루틴이 끝날 때까지 대기

 


혹시 예제로 실제 작업 10개 돌리기, 조건부 Done(), 중첩된 고루틴 대기 등도 보고 싶어?
아니면 지금 구조에서 응용하고 싶은 예시가 있어? 😎

 

 

 

Q3.

포인터는 왜 쓰인거야?

 

 

 

A3.

좋아, 정말 핵심을 짚었어.

WaitGroup 쓸 때 함수 안에 보면 이렇게 돼 있지:

func doTask(id int, wg *sync.WaitGroup)

❓ 여기서 왜 굳이 포인터(*sync.WaitGroup)를 쓰는지 궁금한 거지?

정답부터 말하면:


✅ 이유: WaitGroup은 "값"이 아니라 "상태를 공유"해야 하기 때문이야


🔍 이걸 두 줄로 요약하면:

  1. sync.WaitGroup은 내부에 카운터 상태를 가지고 있는 구조체
  2. 고루틴마다 복사된 구조체를 쓰면 카운터가 제대로 줄어들지 않아

📦 비유로 이해해보기

  • wg.Add(1) → “작업 하나 더 시작했어!”
  • wg.Done() → “작업 하나 끝났어!”

그런데 고루틴 안에서 복사본에다가 Done() 호출하면?
원본 wg는 몰라서 Wait()가 끝나지 않음


❌ 포인터 없이 썼을 때 문제 예시

 
func doTask(id int, wg sync.WaitGroup) {
	defer wg.Done() // ❌ 이건 복사본에 Done을 호출 → main에서 기다리는 wg에는 반영되지 않음
	fmt.Println("작업", id)
}
for i := 0; i < 3; i++ {
	wg.Add(1)
	go doTask(i, wg) // ❌ 값 전달됨
}

🔥 결과:

  • main()은 Wait()에서 계속 기다림
  • 각 고루틴은 복사된 wg를 Done()하지만, 원래 wg는 그대로 남음

✅ 포인터로 넘겼을 때는?

 
func doTask(id int, wg *sync.WaitGroup) {
	defer wg.Done() // ✅ 진짜 wg를 참조해서 Done 호출됨
	fmt.Println("작업", id)
}
go doTask(i, &wg) // ✅ 포인터 전달

✅ 결과:

  • Add() 한 만큼 Done()이 호출되고
  • Wait()는 정확히 끝남

💬 한 줄 요약

WaitGroup은 "상태가 바뀌어야 의미가 있는 구조체"라서, 반드시 포인터로 넘겨야 모든 고루틴이 같은 값을 공유해.


필요하다면 이걸 실험으로 증명해볼 수 있게 포인터 vs 값 비교용 실습 예제도 만들어줄 수 있어.
해볼까? 😎

 
 
 

Q4.

waitGroup에서 add(n)에서 n은 정수인거야? 그리고 add(n)에 들어간 n만큼 wg.done()이 되어야 하는건가?

 
 
 
A4.
정확히 이해하려고 하는 흐름 아주 좋아!

질문 2개로 나눠서 확실하게 정리해줄게.


✅ 1. WaitGroup.Add(n)에서 n은 정수인가?

👉 맞아, int 타입이야.

func (wg *WaitGroup) Add(delta int)
  • delta라는 이름의 정수 파라미터를 받아
  • +n을 넣으면 **“그만큼 고루틴을 기다리겠다”**는 뜻
  • -n을 넣으면 수동으로 줄일 수도 있지만, 보통은 Done()을 씀

✅ 2. Add(n) 했으면 Done()도 n번 해야 해?

👉 정확히 맞아. 반드시 n번의 Done() 호출이 있어야 해.

  • wg.Add(3) 했다면 → 고루틴 3개에서 각각 wg.Done()을 호출해야 함
  • 그래야 wg.Wait()가 끝나

❗ 만약 Add(3) 해놓고 Done()을 2번만 호출하면?

→ Wait()는 영원히 멈춘 채 기다림

❗ 반대로 Add(1) 해놓고 Done()을 2번 호출하면?

panic 발생:

panic: sync: negative WaitGroup counter

✅ 예제로 이해

var wg sync.WaitGroup
wg.Add(3) // 고루틴 3개 기다리겠다

go func() {
	defer wg.Done()
	// 작업 1
}()

go func() {
	defer wg.Done()
	// 작업 2
}()

go func() {
	defer wg.Done()
	// 작업 3
}()

wg.Wait() // ← 위 3개의 Done()이 끝나야 여기서 빠져나옴

 


✅ 실수 주의 정리

실수 결과
Add(3) → Done()을 2번만 호출 Wait()에서 영원히 멈춤 (deadlock)
Add(1) → Done()을 2번 호출 panic: negative WaitGroup counter
Add(0) → Wait()는 즉시 통과 (아무것도 안 기다림)  

✅ 한 줄 요약

WaitGroup.Add(n)은 “n개의 Done을 기다리겠다”는 선언이고,
WaitGroup.Wait()은 “모든 Done이 끝날 때까지 멈춰 있겠다”는 명령이야.


더 복잡한 예시로, 중첩된 고루틴이나 Add()를 고루틴 안에서 사용하는 실전 패턴도 보여줄 수 있어.
해볼까? 😎