Go언어는 경량 스레드로 동시성 프로그래밍을 지원하는 고루틴이라는 기능을 제공합니다. 이를 통하여 효율적인 병행성을 구현할 수 있습니다.
고루틴의 사용법, 고루틴에 사용되는 뮤텍스, 뮤텍스가 발생시킬 수 있는 데드락에 대하여 알아보겠습니다.
목차
고루틴이란?
뮤텍스란?
데드락이란?
고루틴이란?
고루틴은 경량 스레드로서, Go언어의 주요 특징인 동시성을 지원하는 개념입니다. 고루틴은 하나의 스레드에서 관리되며, 작은 작업 단위로 프로그램을 병행 실행할 수 있습니다. main함수도 고루틴에서 실행되므로 프로그램이 실행되면 적어도 하나의 고루틴을 가지고 시작합니다.
예시 코드
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func twoSecondTrigger() {
defer wg.Done()
defer fmt.Println("twoSecondTrigger 종료")
for i:=0 ; i<5 ; i++ {
time.Sleep(time.Second * 2)
fmt.Printf("%d번째 2초 트리거\n",i+1)
}
}
func threeSecondTrigger() {
defer wg.Done()
defer fmt.Println("threeSecondTrigger 종료")
for i:=0 ; i<5 ; i++ {
time.Sleep(time.Second * 3)
fmt.Printf("%d번째 3초 트리거\n",i+1)
}
}
func main() {
wg.Add(2)
go twoSecondTrigger() // 고루틴 작동
go threeSecondTrigger() // 고루틴 작동
wg.Wait()
}
1. 고루틴 생성
고루틴을 실행하는 방법은 함수를 실행할 때 앞에 go 키워드를 쓰고 함수를 호출하면 해당 함수를 수행하는 고루틴을 생성합니다.
2. defer
defer는 함수가 종료될 때 실행시킬 함수를 선언할 때 사용됩니다. defer는 함수 내에서 여러 번 사용 가능하며 defer가 선언된 순서로 stack에 쌓이기 때문에 실행은 아래에 적힌 defer먼저 실행됩니다.
위의 예시에서는 함수가 종료될 때 wg.Done()을 실행하라고 지정하였습니다.
3. WaitGroup
고루틴이 모두 실행되었는지 확인하고, 모든 고루틴이 종료할 때까지 대기하는 데 사용됩니다.
Add : 소괄호 안의 정수만큼 고루틴을 추가합니다.
Done : 고루틴이 작업을 종료했음을 의미합니다. Add(-1)과 동일한 역할을 합니다.
Wait : 모든 고루틴의 작업이 완료되어 Add로 추가한 고루틴의 수가 0이 될 때까지 대기합니다.
출력
1번째 2초 트리거
1번째 3초 트리거
2번째 2초 트리거
3번째 2초 트리거
2번째 3초 트리거
4번째 2초 트리거
3번째 3초 트리거
5번째 2초 트리거
twoSecondTrigger 종료
4번째 3초 트리거
5번째 3초 트리거
threeSecondTrigger 종료
go를 통하여 twoSecondTrigger와 threeSecondTrigger가 병렬로 실행됩니다.
twoSecondTrigger가 2초마다 threeSecondTrigger가 3초마다 출력을 합니다.
병렬로 실행되어 번갈아 출력되다가 twoSecondTrigger가 종료되고 threeSecondTrigger가 진행되다 종료됩니다.
이러한 병렬처리로 작업을 빠르게 리소스를 효율적으로 사용할 수 있습니다.
뮤텍스란?
뮤텍스는 고루틴을 사용 시 특정 변수에 동시에 접근하게 되는 경우 오류가 발생할 수 있습니다. 이러한 동시 접근을 제어하여 경쟁 상태를 방지하는 동기화 기법입니다.
예시코드
동시 접근으로 인한 오류 발생 코드
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
var sharedValue int
for i := 0; i < 5; i++ {
wg.Add(1)
go increment(&sharedValue)
}
wg.Wait()
fmt.Println("Final value:", sharedValue)
}
func increment(value *int) {
defer wg.Done()
newValue := *value + 1
time.Sleep(time.Second) // 작업 시뮬레이션
*value = newValue
}
increment는 실행되면 입력된 value를 1 증가시키고 newValue에 저장하였다가 1초 후에 value에 저장합니다.
병렬로 처리하였기에 모두 value가 1일 때 증가시켜 newValue가 2로 선언되고 1초 후에 value를 2로 설정합니다.
value를 5번 증가시켰지만 value가 2가 됩니다.
mutex로 제어한 코드
var wg sync.WaitGroup
var mu sync.Mutex
func increment(value *int) {
defer wg.Done()
fmt.Println("Lock의 범위가 아닌 곳은 병렬처리")
mu.Lock()
defer mu.Unlock()
newValue := *value + 1
fmt.Println("여기는 순차적인 직렬처리")
time.Sleep(time.Second) // 작업 시뮬레이션
*value = newValue
}
sync.Mutex를 전역변수 mu로 선언 후 increment함수에 mu.Lock과 mu.Unlock을 추가하였습니다.
Lock을 호출하여 뮤텍스를 잠겄고 defer를 통하여 함수가 끝나면 뮤텍스를 해제하는 코드입니다.
mu.Lock을 만나기 전까지의 코드는 병렬처리가 되고 mu.Lock을 만나는 경우 각 고루틴에서 mu.Lock을 만나면 먼저 만나서 작업을 진행 중인 코드 외에 나머지 코드는 대기합니다.
따라서 순차적으로 value를 증가시키므로 value의 결과는 5가 됩니다.
데드락이란?
고루틴에서 뮤텍스를 통하여 작업을 통제하였습니다. 이러한 방법이 또 다른 문제를 발생시킬 수 있습니다.
데드락은 서로 상대방의 작업이 끝나기를 기다리게 되는 상태를 말합니다. 이로 인하여 전체 시스템이 멈추게 됩니다.
두 개의 값을 받아 1 증가시키고 곱하는 코드가 아래와 같이 있습니다.
예시코드
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var mu1 sync.Mutex
var mu2 sync.Mutex
func firstFunc(x, y *int){
defer wg.Done()
fmt.Println("firstFunc 시작")
mu1.Lock()
*x += 1
fmt.Println("x 증가")
time.Sleep(time.Second)
mu2.Lock()
*y += 1
fmt.Println("y 증가")
fmt.Println("firstFunc 결과 : ", *x * *y)
mu1.Unlock()
mu2.Unlock()
}
func secondFunc(x, y *int){
defer wg.Done()
fmt.Println("secondFunc 시작")
mu2.Lock()
*y += 1
fmt.Println("y 증가")
time.Sleep(time.Second)
mu1.Lock()
*x += 1
fmt.Println("x 증가")
mu2.Unlock()
mu1.Unlock()
}
func main(){
fmt.Println("시작")
x := 1
y := 1
wg.Add(2)
go firstFunc(&x, &y)
go secondFunc(&x, &y)
wg.Wait()
}
위와 같이 비슷하지만 순서가 다른 함수가 고루틴을 통하여 실행되면 first는 mu2가 해제되길 기다리고 second는 mu1이 해제되길 기다리는 서로가 서로의 끝을 기다리는 현상이 발생됩니다.
출력
시작
secondFunc 시작
y 증가
firstFunc 시작
x 증가
fatal error: all goroutines are asleep - deadlock!
x와 y를 서로 증가시킨 후 다음 작업을 하기 위하여 끝없이 기다리게 되어 deadlock오류가 발생하게 됩니다.
편리하지만 오류가 발생할 수 있으므로 주의하여 사용하여야 합니다.
'Go language' 카테고리의 다른 글
[Go] 웹 서버 만들기 (net/http, Mux, DefaultServeMux) (0) | 2023.09.01 |
---|---|
[Go] 채널 이해하기 (콘텍스트) (0) | 2023.08.28 |
[Go] 인터페이스 (어댑터) (0) | 2023.08.26 |
[Go] 메서드 사용법 (함수와 메서드의 차이) (0) | 2023.08.25 |
[Go] Pointer이해하기 (call by value 주의사항) (0) | 2023.08.25 |