공유 변수에 대한 정확한 접근을 구현하기 위해 엄수해야 하는 세세한 내용들은 다양한 환경에서 동시성 프로그래밍을 어렵게 했다. Go는 공유 변수가 채널을 돌려가며 전달된다는 점에서 다른 접근을 권장한다.
공유 변수는 개별 쓰레드 실행에 의해서 결코 공유되지 않는다. 언제든지 하나의 고루틴이 값에 접근하기 때문에 데이터 경쟁은 구현 설계상 발생할 수 없다.
공유 메모리로 통신하지 말라. 대신 통신으로 메모리를 공유하라
단일 cpu에서 실행되는 전형적인 단일 쓰레드 프로그램을 떠올려보면, 여기에는 동기화를 위한 기본 자료형이 필요 없다. 새로운 인스턴스를 실행시켰을 때 그 두개를 통신하는데, 통신 자체가 동기화 장치인 경우, 여전히 다른 동기화가 필요없다.
이는 유닉스 파이프 라인과 완벽하게 들어 맞는다. 동시성에 대한 Go의 접근 방식이 Hoare의 통신 순차적 프로세스 (csp)에서 비롯되었지만, 타입 안전이 보장되는 식의 일반화된 유닉스 파이프라고 볼 수 있다.

쓰레드, 코루틴, 프로세스 등 기존의 용어는 부정확한 함의를 전달하기 때문에 Go루틴이라고 부른다. Go루틴은 단순한 모델이다. 같은 주소 공간에서 다른 고루틴과 동시에 실행되며, 가볍고, 비용이 적게든다. 그리고 필요한만큼 힙 스토리지를 할당(또는 해제)하여 커진다.
Go루틴은 Go 런타임이 관리한 LighWeight 논리적(가상) 쓰레드이다. Go 에서 "go" 키워드를 사용하여 함수를 호출하면, 런타임시 새로운 goroutine을 실행한다.
goroutine은 비동기적으로 함수 루틴을 실행하므로, 여러 코드를 동시에 실행하는데 사용된다.
go list.Sort() // list.Sort() 의 정렬을 완료될 때 까지 기다리지 말고 다른 함수 실행
goroutine은 OS쓰레드보다 훨씬 가볍게 비동기 (병렬 처리)를 구현하기 위해 만든 것으로, 기본적으로 Go 런타임이 자체 관리한다. Go 런타임 상에서 관리되는 작업단위인 여러 goroutine들은 종종 하나의 OS 쓰레드 1개로도 실행되곤 한다.
즉 Go routine들은 OS쓰레드와 무조건 적으로 1대 1대응은 아닌 것이다. Multplexing으로 훨씬 적은 OS 쓰레드들을 사용한다. 메모리 측면에서도 OS쓰레드가 1 메가바이트의 스택을 갖는 반면, goroutine은 이보다 훨씬 작은 몇 킬로바이트의 스택을 갖게 된다 (필요시 동적으로 증가하기도 함). Go 런타임은 Go 루틴을 관리하면서 Go채널을 통해 Go루틴 간의 통신을 쉽게 할 수 있도록 하였다.
package goroutine
import (
"fmt"
"time"
)
func Say(arg string) {
fmt.Println(arg)
}
func Goroutine() {
Say("1번")
go Say("2번")
go Say("3번")
go Say("4번")
go Say("5번")
time.Sleep(1 * time.Second)
}
위 예제에서 함수를 보면, 첫번째 say() 함수를 동기적으로 호출한 뒤, 다음으로 goroutine을 이용해 say()함수를 비동기 적으로 4번 호출하고 있다. 첫번째 동기적 호출 say()함수가 완전히 끝난 후 다음 문장으로 이동하게 되고, 4개의 비동기 호출은 별도의 Go루틴들에서 동작하게 되면서, 메인루틴은 계속 다음 문장 time.Sleep를 실행하게 된다.
여기서 goroutine들은 비동기적으로 동작하게 되므로 실행순서가 일정하지 않으므로 프로그램 실행시마다 다른 출력 결과를 나타나게 된다. 또한 time.Sleep가 없을 경우, 비동기적으로 실행한 함수가 호출이 되기전에 프로그램이 종료될 수 있으므로, goroutine이 종료될 때 까지 기다려주는 함수가 필요할 것이다.
Go루틴은 익명함수에 대해 사용할 수도 있다. 타 익명함수와 같이 go 키워드 뒤에 익명함수를 바로 정의하는 것으로, 이 익명함수를 비동기로 실행하게 된다. 또한 만약 익명함수에 파라미터가 존재하는 경우 go 익명함수 바로 뒤에 파라미터를 전달하게 해준다.
항상 go는 익명함수 시 함수를 호출해야 함 go func()()
package gorutine
import (
"fmt"
"sync"
"time"
)
func Say(arg string) {
fmt.Println(arg)
}
func Gorutin() {
Say("1번")
go Say("2번")
go Say("3번")
go Say("4번")
go Say("5번")
time.Sleep(1 * time.Second)
}
func Goroutine() {
var wait sync.WaitGroup
wait.Add(2)
go func() {
defer wait.Done()
fmt.Println("Done")
}()
go func(msg string) {
defer wait.Done()
fmt.Println(msg)
}("Hi")
wait.Wait()
}
위의 예제에서 sync.WaitGroup()를 사용하고 있는데, 이는 time.Sleep과 같이 몇 초를 기다리는 것이 아닌, 여러 Go루틴들이 끝날 때까지 기다리는 역할을 한다. WaitGroup을 사용하기 위해서는 먼저 Add() 메서드에 몇 개의 Go루틴을 기다릴 것인지 지정해 줄 수 있다.
각 Go루틴에 대해 Done() 메서드를 호출한다. 이는 defer에 담을 수도 있다. 그리고 메인 루틴에서는 Wait()메서드를 호출해, Go 루틴들이 모두 끝나기를 기다린다.
Go는 디폴트로 1개의 CPU를 사용한다. 즉 여러 개의 Go 루틴을 만들더라도, 1개의 Cpu에서 작업을 시분할하여 처리한다. (동시성 처리) 만약 머신이 복수개의 CPU를 가진 경우, Go 프로그램을 다중 CPU에서 병렬처리하게 할 수 있는데, 병렬처리를 위해서는 runtime.GOMAXPROCS(CPU수) 메서드를 호출하여야 한다. (CPU수는 Logical CPU수를 가리킨다)
package main
import (
"runtime"
)
func main() {
// 4개의 CPU 사용
runtime.GOMAXPROCS(4)
//...
}
동시성 처리와 병렬처리는 비슷하게 보이지만, 전혀 다른 개념이다
프로그래밍에서 동시성은 독립적으로 실행되는 프로세스의 구성인 반면, 병렬성은 계산의 동시 실행입니다. 동시성은 한 번에 많은 일을 처리하는 것입니다. 병렬화는 한 번에 많은 일을 하는 것입니다.