main_function

[Go/Golang] 1. 동시성(Concurrency) 본문

Programming

[Go/Golang] 1. 동시성(Concurrency)

MAIN 2021. 1. 13. 02:11

Go언어를 통해 동시성의 개념과 Go의 동시성 관점에서의 장점을 알아보려고 합니다. 

동시성 소개를 시작으로 Go 언어에서 동시성을 다루는 방식과 고루틴의 상세한 소개 등을 시리즈 게시물로 다루려고 합니다.

 

많은 사람들이 '동시성(concurrency)'이란 단어를 들으면 '병렬성(parallelism)'으로 생각하는 경우가 많습니다. 

하지만 Concurrency != Parallelism 입니다.

이 부분은 Go 언어의 창시자 중 한 분인 Rob Pike가 강연한 내용이 Go 블로그에 있으니 나중에 한번 참고해보시는 것도 좋습니다.

(blog.golang.org/waza-talk, vimeo.com/49718712)

 

 

동시성은 프로세스를 실행하는 독립적인 구성 / 병렬성은 관련있는 계산을 동시에 실행

동시성은 한 번에 여러 개의 것들을 다루는 것 / 병렬성은 한 번에 여러 일을 동시에 하는 것

concurrency is about structure / parallelism is about execution

 

위의 구문들을 해석하면 다음과 같습니다.

동시성은 개발된 소스코드 자체의 속성이고, 병렬 처리는 실행 중인 프로그램의 속성(프로그램 실행의 형태)입니다.

 

다시 말해 우리는 병렬적인 코드를 작성하는 것이 아니라, 병렬로 실행되기를 바라면서 동시성 코드를 작성하는 것입니다.

 

그렇기 때문에 일반적으로는 병렬성 보단 동시성이 맞습니다. (물론 병렬 처리를 위한 lib 등에선 이를 위한 특별한 처리들을 하고 있지만요.)

병렬적으로 실행되길 바라지만 만약 환경이 싱글 코어, 싱글 프로세스라면? (극단적 가정)

극단적인 가정

즉 코드를 병렬적으로 실행하기 위해 작성해도 프로그램이 실행될 때 환경에 따라 실제로 그렇게 되리라는 보장이 없다기 때문에 동시성과 병렬성을 위와 같이 구분하는 것입니다.

정리하면,

  1. 우리는 병렬적인 코드는 작성하는 것이 아니라, 병렬로 실행되기를 바라면서 동시성 코드를 작성하는 것입니다.
  2. 동시성 코드가 실제로 병렬로 실행되는지 여부를 모를 수도 있습니다.
  3. 병렬 처리인지 아닌지는 시간 또는 Context에 의해 결정됩니다. 
    (Context란? 어떤 연산을 원자적이라고 판단할 수 있는 범위를 Context라고 정의)

 

 

동시성을 어렵게 하는 것들

- Race Condition

Race condition은 둘 이상의 작업이 올바른 순서로 실행되어야 하지만 코드가 그렇게 작성되지 않아서 이 순서가 보장되지 않을 떄 발생합니다. 대부분 변수를 읽거나 쓰려고 할 때 발생하는 Data Race 입니다.

var data int
go func() {
    data++
}()
if data == 0 {
    fmt.Printf("%v", data)
}
  1. 아무것도 출력되지 않을 때 : 고루틴에서 data++을 이미 실행
  2. 0이 출력될 때 : 고루틴 보다 data의 값 체크 및 프린트가 먼저 실행
  3. 1이 출력될 때 : data == 0으로 체크했지만 프린트가 되기 전 고루틴에서 data++을 실행
var data int
go func() {
    data++
}()
time.Sleep(1 * time.Second) // 추가
if data == 0 {
    fmt.Printf("%v", data)
}

중간에 인위적으로 텀을 두어서 코드를 작성한다면? => data race의 가능성만 약간 낮출 뿐 올바르지 않습니다.

 

- Atomicity

동작하는 컨텍스트(정의된 범위) 내에서 분기가 되거나 중단되지 않는 성질. 하지만 어떤 컨텍스트에서는 원자적인 것이 다른 컨텍스트에서는 아닐 수 있습니다.

i++
// i의 값을 가져온다
// i의 값을 증가시킨다
// i의 값을 저장한다

예를 들어 특정 프로세스의 컨텍스트 내에서 원자적이었지만 OS의 컨텍스트에서는 그렇지 않은 경우입니다.

 

- 메모리 접근  동기화

다수의 프로세스가 동일한 메모리 영역에 접근하려는 경우, 메모리 접근 동기화 또한 동시성을 어렵게 만드는 원인 중 하나입니다.

var data int
go func() {
    data++ // critical section1
}() 
if data == 0 { // critical section2
    fmt.Println("the value is 0")
} else {
    fmt.Printf("the value is %v", data) // critical section3
}

위 코드는 완전히 비결정적인 코드입니다. 크리티컬 섹션이 3개로 나눠집니다.

  • data 변수를 증가시키는 고루틴
  • data 값이 0인지 확인하는 if 구문
  • 출력하기 위해 data의 값을 가져오는 Printf 구문

 

- Deadlock

교착 상태. 동시에 실행 중인 서로 다른 프로세스가 서로 상대방의 작업이 끝나기 기다리는 현상입니다. 나무위키에도 나온 아래 시가 잘 표현해준다고 생각합니다.

먼 길

아기가 잠드는 걸
보고 가려고
아빠는 머리맡에
앉아 계시고.
아빠가 가시는 걸
보고 자려고
아기는 말똥말똥
잠을 안 자고.

- 윤석중

아래 조건일 때 발생합니다.

  • 상호 배제(Mutual exclusion)
  • 점유대기(Hold and wait)
  • 비선점(No preemption)
  • 순환대기(Circular wait)

 

- Livelock

서로 다른 프로세스가 잠금과 해제를 무한 반복하는 상태입니다. 즉, 무언가 연산을 일어나고 있지만 생산적이거나 개발자가 의도하는 결과에는 도달하지 못하는 상태이죠.

예를 들어 좁은 골목에서 다른 사람을 만나 서로 비켜주면서 양보해주는 상황인거죠. 아래 과정이 무한 반복으로 일어나는 상황입니다.

  • 상대방에게 양보하기 위해 왼쪽으로 이동 - 상대도 같은 방향으로 이동
  • 이를 보고 다시 오른쪽으로 이동 - 상대도 같은 방향으로 이동

 

- Starvation

말그대로 프로세스가 필요한 자원을 가져오지 못하는 상황입니다. 기아 상태는 보통 스케줄링이나 상호 배제 알고리즘의 오류로 인해 생기는 경우도 있지만 자원 누수로 인해 발생할 수도 있습니다.

욕심쟁이 작업자와 양보하는 작업자를 구현한 아래 코드로 기아 상태를 일부 재현할 수 있습니다.

package main

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

var wg sync.WaitGroup
var sharedLock sync.Mutex

const runtime = 1 * time.Second

func main() {
	greedyWorker := func() {
		defer wg.Done()

		var count int
		for begin := time.Now(); time.Since(begin) <= runtime; {
			sharedLock.Lock()
			time.Sleep(4 * time.Nanosecond)
			sharedLock.Unlock()


			count++
		}
		fmt.Printf("Greedy worker was able to execute %v work loops\n", count)
	}


	politeWorker := func() {
		defer wg.Done()

		var count int
		for begin := time.Now(); time.Since(begin) <= runtime; {
			sharedLock.Lock()
			time.Sleep(1 * time.Nanosecond)
			sharedLock.Unlock()

			sharedLock.Lock()
			time.Sleep(1 * time.Nanosecond)
			sharedLock.Unlock()

			sharedLock.Lock()
			time.Sleep(1 * time.Nanosecond)
			sharedLock.Unlock()

			sharedLock.Lock()
			time.Sleep(1 * time.Nanosecond)
			sharedLock.Unlock()

			count++
		}
		fmt.Printf("Polite worker was able to execute %v work loogs\n", count)
	}

	wg.Add(2)

	go politeWorker()
	go greedyWorker()

	wg.Wait()

}

// Greedy worker was able to execute 705115 work loops
// Polite worker was able to execute 362920 work loogs

크리티컬 섹션이 끝났음에도 불구하고 불필요하게 lock을 걸어서 기아 상태를 유발시키고 있습니다. 실제 코드에서는 무엇이 좋다 나쁘다를 판단하기엔 힘들 수도 있습니다. 따라서 상황에 알맞게 선택해야 할 것 같습니다.

성능을 위해 큰 범위를 동기화할지   VS   공정성을 위해 세분화된 범위를 동기화할지

 

 


Reference

medium.com/@k.wahome/concurrency-is-not-parallelism-a5451d1cde8d

 

Concurrency is not Parallelism

“Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.” — Rob Pike

medium.com

www.yes24.com/Product/Goods/74820845

 

Go 동시성 프로그래밍

고성능의 멀티 코어 CPU, 클라우드 기반의 비동기 서비스 등 최근 트렌드를 고려하면 프로그램을 작성할 때 동시성을 고려하는 것은 필수 과정이다. 이 책에서는 Go 언어의 동시성 모델과 이론적

www.yes24.com

blog.golang.org/waza-talk

 

Concurrency is not parallelism - The Go Blog

Andrew Gerrand 16 January 2013 If there's one thing most people know about Go, is that it is designed for concurrency. No introduction to Go is complete without a demonstration of its goroutines and channels. But when people hear the word concurrency they

blog.golang.org

 

'Programming' 카테고리의 다른 글

[Go/Golang] 채널(channel)  (0) 2021.01.20
[Go/Golang] 2. 고루틴(goroutine) Deep Dive  (0) 2021.01.13
Go 언어(golang)의 특징  (0) 2021.01.09
Comments