concurrency in action - chapter 5

59
NHN NEXT 1기 이진우 ([email protected]) C++ 메모리 모델과, Atomic type 연산 Chapter 5

Upload: jinwoo-lee

Post on 08-Aug-2015

103 views

Category:

Technology


3 download

TRANSCRIPT

Page 1: Concurrency in action - chapter 5

NHN NEXT 1기 이진우 ([email protected])

C++ 메모리 모델과, Atomic type 연산

Chapter 5

Page 2: Concurrency in action - chapter 5

문제는 메모리

왜 메모리가 문제인가?

Page 3: Concurrency in action - chapter 5

변수는 메모리에 저장됩니다

직접 메모리를 읽고 쓴다고 생각하시겠지만…

Page 4: Concurrency in action - chapter 5

CPU 요즘 CPU에는,

L1, L2, L3 캐쉬까지 있습니다

(사진 속 L1, L2는 코어 안에…)

Page 5: Concurrency in action - chapter 5

메모리는 멀고 느립니다

버스 타고 다녀야 해요

그래서 매번 메모리를 직접 읽고 쓰지는 않습니다

Page 6: Concurrency in action - chapter 5

이 챕터에서 해결 할 문제들

멀티 쓰레딩은 쉽지 않습니다

동시에 접근하면 무슨 일이? (Data race)

전부 Lock을 쓰면 느려지지 않을까? (Lock-free)

쓰레드 사이의 자료구조는? (동기화 문제)

Page 7: Concurrency in action - chapter 5

메모리 모델

• C++11 표준의 중요한 변화

– 멀티 쓰레딩 고려 (이전엔 고려하지 않음)

• 대부분의 프로그래머는 인식하지 못함

– 그냥 mutex나 condition variable 사용

– 저게 “왜” 동작하는지는 중요하지 않음

• 그런데 왜 알아야 하나?

– 고성능을 위해서 : Low-level 접근 필요

Page 8: Concurrency in action - chapter 5

C++ 표준 위원회의 생각

성능을 위해 low level 언어를 사용 할 필요가 없도록 하자.

Low-level 동기화 기능 지원 - CPU 명령을 절약할 수 있음

Page 9: Concurrency in action - chapter 5

Object & Memory location

• Object : C++에서 데이터의 구성 단위

– 객체지향에서의 “object”와는 다른 용어

– 변수도 object, 구조체도 object

– 구조체 안의 멤버도 각각 하나의 object

• 모든 Object는 Memory location을 가짐

– 심지어 0바이트 크기를 가진 object 조차도…

• 기본 데이터 형 (int, char)

– 정확히 하나의 memory location (크기 무관)

Page 10: Concurrency in action - chapter 5

Undefined behavior

Data race 하나라도 Atomic이 아닌 연산이 있고,

하나라도 쓰기 연산이 있다면,

Undefined behavior ( 동작이 정의되어 있지 않음 )

Thread 1

Thread 2 (같은 위치)

Thread 3

동시 접근

Page 11: Concurrency in action - chapter 5

Undefined behavior

전체 프로그램이 그 무엇도 예측할 수 없는 상태가 된다

컴파일러나 운영체제의 책임 한계를 벗어난 영역

Page 12: Concurrency in action - chapter 5

수정 순서 (Modification order)

실행 할 때 마다 바뀔 수 있지만, (non-deterministic)

Atomic도 아니고 프로그래머가 순서를 맞춰주지 않으면,

data race 발생으로 undefined behavior

Thread 1 write

Thread 3 write

Thread 1 write

Thread 2 write

Thread 4 write

Thread 1 write

Thread 3 write

Thread 3 write

Thread 2 write

“Object A” (시간)

Thread 1

Thread 4

Thread 2

Thread 3

Page 13: Concurrency in action - chapter 5

수정 순서 (Modification order)

(2)에서 읽은 값이 (1)에서 쓰여진 값이라면,

(3)의 쓰기 작업은 (1)보다 나중이다. (쓰레드 1에서만 보장)

Atomic 타입이거나 단일 쓰레드에서는,

이런 것들이 추가적인 장치 없이 항상 보장 되지만,

멀티 쓰레드에서는 동기화를 신경 써야 한다.

Object A 읽기(2)

Object A 쓰기(3)

쓰레드 1 (시간)

Object가 Atomic type이 아닐 때,

프로그래머가 동기화의 책임이 있음

Object가 Atomic type일 때,

컴파일러가 필요한 작업을 해 줌

Object A 쓰기(1)

다른 쓰레드

Page 14: Concurrency in action - chapter 5

Atomic Operation (원자적 연산)

• Atomic(원자적)이란? – 더 이상 쪼개질 수 없고 독립적으로 실행

– 부분적으로 실행된 상태는 존재하지 않음

– Data race를 방지

• C++ 에서는… – Atomic type 변수를 선언하여 사용

Page 15: Concurrency in action - chapter 5

Atomic 없이 사용하면?

Non-atomic 쓰기

쓰는 도중에

다른 쓰레드에서

엉뚱한 값을 읽음

Non-atomic 읽기

일부분을 읽은 후

다른 쓰레드에서 수정

이후 나머지 부분을 읽음

이런 문제가 생길 수도 있음

(캐시 라인에 걸쳤거나 캐시 라인보다 큰 object)

※ data race 문제와는 별개 (이 쪽이 진짜 문제)

Page 16: Concurrency in action - chapter 5

표준 Atomic type

std::atomic<bool> atomic_bool std::atomic<long> atomic_long

std::atomic<char> atomic_char std::atomic<signed char> atomic_schar

std::atomic<int> atomic_int std::atomic<unsigned char> atomic_uchar

다른 이름들…

이 외에도 다 있지만 그냥 std::atomic<> 쓰세요…

Page 17: Concurrency in action - chapter 5

• 실행 중에 확인 할 수 있음 – true : atomic instruction 사용

– false : 에뮬레이션 (내부적으로 lock 사용)

lock-free 체크

Page 18: Concurrency in action - chapter 5

std::atomic_flag

• 단순한 사용법

• lock-free 보장 – 따라서 lock-free 체크 메소드가 아예 없음

Page 19: Concurrency in action - chapter 5

이것은 Spinlock이 아닙니다

일반 변수를 이용하여 Spinlock 구현하려고 한 예

정상적으로 동작하지 않음

Page 20: Concurrency in action - chapter 5

이것은 Spinlock이 아닙니다

두 쓰레드가 동시에 변경 가능

Page 21: Concurrency in action - chapter 5

Spinlock

std::atomic_flag 를 이용한 spinlock 구현

( while문을 빠져 나오는 쓰레드는 한번에 하나만 가능 )

Page 22: Concurrency in action - chapter 5

std::atomic<bool>

• 가장 기본적인 타입 – 기본적인 모든 기능들 존재

– load(), store(), exchange(), compare_exchange_weak(), compare_exchange_strong()

Page 23: Concurrency in action - chapter 5

Compare-and-swap (CAS)

• 조건부 swap

– 변수의 값과 주어진 값을 비교 후 교환

– 원래 값이 주어진 값일 때만 교환 되는 것이 보장됨

• compare_exchange_weak()

– 변수의 값이 다르면 : 실패

– 변수의 값이 같으면 : swap, (그런데 실패 할 수도 있음)

• compare_exchange_strong()

– 변수의 값이 다르면 : 실패

– 변수의 값이 같으면 : swap

Page 24: Concurrency in action - chapter 5

Compare-and-swap 활용

while문을 이용하여 성공할 때까지 반복

누군가가 중간에서 값을 바꾸면, 실패하기 때문에 반복

Page 25: Concurrency in action - chapter 5

Weak? Strong?

Weak : num이 x와 같아도 실패 할 수 있다

Strong : num이 x와 다른 경우에만 실패한다

x86 계열 CPU : 둘 다 strong 으로 동작한다

Weak ordered machine : strong은 loop로 구현되어 있다

Page 26: Concurrency in action - chapter 5

std::atomic<T*>

• 기본적인 사용법은 동일 – load(), store(), exchange(),

compare_exchange_weak(), compare_exchange_strong() 등등…

• 증감 연산 가능

Page 27: Concurrency in action - chapter 5

std::atomic<(정수형)>

• 기본적인 사용법은 동일 – load(), store(), exchange(),

compare_exchange_weak(), compare_exchange_strong() 등등…

• 거의 모든 연산 사용 가능 – fetch_add(), fetch_sub(), fetch_and(),

fetch_or(), fetch_xor(),

– +=, -=, &=, |=, ^=, ++x, x++, --x, x--

– 없는 것들은 compare_exchange_weak() 쓰면 됨

Page 28: Concurrency in action - chapter 5

std::atomic<(기타)>

• User Defined Type (사용자정의타입) – 가상 함수나 가상 클래스 사용 불가

– 기본 복사 생성자만 사용

– 비교 연산은 memcmp()와 같아야 한다.

• float, double – 값이 같아도 compare_exchange가 실패할 수 있다.

• 워드 크기의 (32비트 머신은 32비트) UDT – lock-free일 수 있다

– 워드의 두 배 크기까지 되는 머신이 가끔 있음

Page 29: Concurrency in action - chapter 5

연산자들 atomic_flag

atomic<bool>

atomic<T*>

atomic <정수형>

atomic<기타>

test_and_set() v

clear() v

is_lock_free() v v v v

load() v v v v

store() v v v v

exchange() v v v v

compare_exchange_weak(), compare_exchange_strong()

v v v v

fetch_add, += v v

fetch_sub, -= v v

fetch_or, |= v

fetch_and, &= v

fetch_xor, ^= v

++, -- v v

Page 30: Concurrency in action - chapter 5

동기화 & 강제 순서 지정

①번과 ④번에서 같은 데이터에 대한 non-atomic 접근이,

②, ③번의 Atomic 변수 “ready”에 대한 연산으로 의해

강제로 순서가 지정된다.

Page 31: Concurrency in action - chapter 5

Happens-before 관계

① happens-before ②

③ happens-before ④

Page 32: Concurrency in action - chapter 5

Synchronizes-with 관계

③에서 읽은 ready가 true가 되면,

write(②) synchronizes-with read(③)가 되면서,

happens-before 관계를 만들게 됨.

Page 33: Concurrency in action - chapter 5

Happens-before는 이행 관계(transitive)

① happens-before ②, ③ happens-before ④,

그런데 ready가 true이면 ② happens-before ③,

따라서 ① happens-before ④.

Page 34: Concurrency in action - chapter 5

Synchronizes-with 관계

Atomic 타입 연산으로만 얻어짐

“적절한 태그” memory ordering (뒤에 나옴)

(적절히 태그된) Write on X

(적절히 태그된) Read X

Read-modify-write on X

쓰레드 2 쓰레드 3

Write on X

or

or

Page 35: Concurrency in action - chapter 5

메모리 순서 정렬 태그

• 크게 세가지로 구분 – sequentially consistent (순차 일관성)

– acquire-release

– relaxed

• 각각 비용이 다를 수 있음 – 이 부분은 CPU 아키텍처에 따라 달라짐

– relaxed acq_rel 혹은 acq_rel seq_cst : 동기화 명령이 추가되는 경우도 있다

– CPU가 많아지면 실행 오버헤드가 커질 수 있다

– 반면 x86 등은 오버헤드가 상대적으로 적다

Page 36: Concurrency in action - chapter 5

Strict consistent

• 책에 없는 옵션 (C++에는 존재하지 않음) – 어떠한 상황에서도 모든 쓰레드가 가장 최근의

write를 읽을 수 있어야 함

Page 37: Concurrency in action - chapter 5

Sequentially consistent

• 순차 일관성 – 가장 직관적이며 이해하기 쉬움

– 모든 atomic 연산의 기본값

• 전역적으로 일관된 수정 순서가 지켜짐 – 어떤 쓰레드에서 보여지는 수정 순서에 대해,

– 다른 쓰레드에서도 그 순서로 보여짐

• 비용 문제 – weakly order machine(alpha 등)에서 고비용

– x86에서는 낮은 편 (store만 약간의 overhead)

memory_order_seq_cst

Page 38: Concurrency in action - chapter 5

Sequentially consistent

동시에 저장된다면,

(true, true), (true, false), (false, true), (false, false) 모두 나올 수 있다.

x.store(true) y.store(true)

x.load()

y.load()

쓰레드 1 쓰레드 2 쓰레드 3

Page 39: Concurrency in action - chapter 5

Sequentially consistent

(true, false)가 두 쓰레드에서 동시에 나올 수 있을까?

x.store(true) y.store(true)

x.load()

y.load()

y.load()

x.load()

쓰레드 1 쓰레드 2 쓰레드 3 쓰레드 4

Page 40: Concurrency in action - chapter 5

Sequentially consistent 만약 쓰레드 3에서 (true, false)라고 나온다면,

x에 대한 수정이 y에 대한 수정보다 먼저이기 때문에,

쓰레스 4에서는 (true, false)가 나올 수 없음.

x.store(true) y.store(true)

x.load() true

y.load() false

y.load()

x.load()

쓰레드 1 쓰레드 2 쓰레드 3 쓰레드 4

Page 41: Concurrency in action - chapter 5

Relaxed

• Synchronizes-with 관계를 만들지 않음 – Atomic함을 보장하는 최소한의 태그

• 연산들의 수정 순서가 바뀔 수 있음 – 변수의 수정 사항이 지연되어 전파

memory_order_relaxed

Page 42: Concurrency in action - chapter 5

Relaxed

(true, false)가 나올 수 있다.

나중에 수정된 y의 값이 보였음에도, 먼저 수정된 x의 값은 아직 적용되지 않았을 수 있다.

x.store(true)

y.store(true)

y.load()

x.load()

쓰레드 1 쓰레드 2

No Sync-with

Page 43: Concurrency in action - chapter 5

Relaxed

(true, false)가 나올 수 없다.

Happens-before 관계는 유지

x.store(true)

y.store(true)

y.load()

x.load()

쓰레드 1 쓰레드 2

Any Sync-with

Page 44: Concurrency in action - chapter 5

Relaxed

(2, 1)은 나올 수 없다.

동일 쓰레드, 동일 변수 - 수정 순서 역전은 있을 수 없다.

x.store(1)

x.store(2)

x.load()

x.load()

쓰레드 1 쓰레드 2

Page 45: Concurrency in action - chapter 5

Acquire-release

• 부분적 일관성 – Acquire 태그 load, Release 태그 write 사이의

– Synchronized-with 관계만 보장

• 전역적 일관성은 보장하지 않음 – 다른 쓰레드에서는 다른 순서로 보여질 수 있음

• x86 프로세서에서는 – Relaxed 대비 동기화 명령이 추가되지 않음

(Reordering만 방지되는 정도)

memory_order_acquire memory_order_release

Page 46: Concurrency in action - chapter 5

Acquire-release

동시에 (true, false)가 나올 수 있다.

Sequentially consistent와 달리,

쓰레드 3과 쓰레드 4가 다른 순서를 볼 수 있다.

x.store(true) y.store(true)

x.load() true

y.load() false

y.load() true

x.load() false

쓰레드 1 쓰레드 2 쓰레드 3 쓰레드 4

Page 47: Concurrency in action - chapter 5

Acquire-release

(false, true)는 나올 수 없다.

Relaxed와 달리, Synchronized-with 관계.

x.store(true)

y.store(true)

쓰레드 1 쓰레드 2

y.load()

x.load()

Page 48: Concurrency in action - chapter 5

Acquire-release

Read(Acquire) 배리어 이후의 연산들과

Write(Release) 배리어 이전의 연산들에 대해

서로 순서가 바뀌지 않도록 보장한다.

Write <Release>

쓰레드 1 쓰레드 2

Read <Acquire>

Page 49: Concurrency in action - chapter 5

Consume-release

• 더 제한적인 부분적 일관성 – Acquire-release와 같으나,

– 데이터 의존성이 있는 변수들만 보장.

• 데이터 의존성? – (write) 변수에 값을 쓰기 위해 사용된 변수

– (read) 변수에 쓰여진 값을 사용한 변수

memory_order_consume memory_order_release

Page 50: Concurrency in action - chapter 5

Consume-release

변수 a, b 에 대해서만 happens-before 관계가 성립

※ std::kill_dependency(p) : 의존성 없이 값 사용

Write p <Release>

쓰레드 1 쓰레드 2

Read p <Consume>

b = *p

p = &a

Page 51: Concurrency in action - chapter 5

Consume-release

Page 52: Concurrency in action - chapter 5

Memory ordering

적용 범위

memory_order_relaxed -

memory_order_consume 읽기

memory_order_acquire 읽기

memory_order_release 쓰기

memory_order_acq_rel 읽기 + 쓰기

memory_order_seq_cst -

Page 53: Concurrency in action - chapter 5

Memory ordering

Store Operation

Load Operation

Read-modify-write Operation

memory_order_relaxed v v v

memory_order_consume x v v

memory_order_acquire x v v

memory_order_release v x v

memory_order_acq_rel x x v

memory_order_seq_cst v v v

사용 가능 여부

Page 54: Concurrency in action - chapter 5

Fence

• 메모리 장벽 (Memory barrier) – Atomic 변수를 수정하지 않고 메모리 순서 보장

– Relaxed 및 non-atomic 변수들을 이용 가능

Page 55: Concurrency in action - chapter 5

간단 요약

지금까지 배운 내용

Page 56: Concurrency in action - chapter 5

Undefined Behavior

어떤 값이 나올지, 무슨 일이 일어날지 보장 할 수 없음

Page 57: Concurrency in action - chapter 5

Behavior defined (atomic, ordering)

항상 같은 결과가 나오는 것은 아니지만,

(0,0), (0,17), (37,17) 의 결과 중 하나

Page 58: Concurrency in action - chapter 5

Atomic, not ordering

순서를 보장해 주지 않음. (대신 빠름)

(0,0), (0,17), (30,0), (37,17) 의 결과 중 하나

Page 59: Concurrency in action - chapter 5

Ordered loads & stores

최소한의 오버헤드

(0,0), (0,17), (37,17) 의 결과 중 하나