Post on 14-Apr-2017
blocking
• 작업이 끝날 때 까지 멈추는 것을 뜻함
–끝날 때 까지 함수가 리턴 되지 않음
– OS가 쓰레드를 멈출 수 있음 (I/O 작업 등...)
• 자료구조 및 알고리즘에서는,
–뮤텍스, 상태변수, std::future 등을 쓰는 경우
nonblocking 자료구조의 종류
• Obstruction-free –독립적 실행 중에는 일정 스텝 안에 종료 보장
• Lock-free –실질적으로 (extreamly often) 일정 스텝 안에 종료 보장
• Wait-free –반드시 항상 일정 스텝 안에 종료 보장
출처 : The Art of Multiprocessor Programming
Spin-lock
• blocking 함수 호출은 없다
• lock-free는 아니다
...이런 상황이 생기기도 합니다.
Thread 1
Thread 2 spin spin spin spin spin spin…
SpinLock잠금
쓰레드 suspend
Lock-free 자료구조
• 다수의 쓰레드가 자료구조에 접근 가능
–최소한 하나의 쓰레드는 항상 전진함을 보장
–다른 쓰레드가 동일한 작업 중 멈췄을 때
• 항상 성공하면 : lock-free
• 멈출 수 있으면 : lock-free가 아님
• 너무 많은 쓰레드가 경쟁하면?
– starvation 발생 가능
Wait-free 자료구조
• 정해진 시도 횟수 안에 완료
–무한히 많은 쓰레드가 경쟁 해도 시간 내 완료
–모든 쓰레드가 항상 전진함을 보장
• 구현이 매우 어렵다
• 실질적으로 성능이 좋지는 않다
Compare-and-swap
• 상당수의 Lock-free 알고리즘이 CAS를 사용
–일반적으로는 single-word CAS를 의미
• Double-word CAS
– 32bit 시스템에서는 64bit CAS
– 64bit 시스템에서는 128bit CAS
–최신 CPU에서만 일부 지원
–컴파일러 옵션 설정이 필요한 경우가 있음
Compare-and-swap
• C++11 에서 구현 된 형태 • 주의) 일반적인 CAS 인터페이스와 약간 다름
• expected가 swap 실패 시 최근 값으로 업데이트
– std::atomic<T>
(예제 코드를 이해하려면 알아야 합니다)
stack::push()
Step 2 + race condition
A B
N
Head
X
문제점 Head가 중간에 바뀌는 문제
해결 방법 : Compare and swap Head가 바뀌면 Step 2,3 재시도
stack::push()
• 구현 코드
– node 생성
– next 포인터를 현재 head로 설정
– head 포인터를 node로 설정
Race condition! 다른 쓰레드가 head 변경 가능
(1)
(2)
(3)
stack::pop() 문제점 #2
• 문제점 2
–메모리 누수 (node를 delete 하지 않음)
– delete 되면, 메모리 참조 및 ABA 문제 발생
–메모리 해제(reclaim) 기법 필요
해제 된 메모리 참조 위험
다른 쓰레드에서 delete 되면?
ABA 문제
B
Head
A
C
Head
A
(1) pop 중인 쓰레드 : 주소 A와 B 확인, head.CAS( A B ) 함수 호출
(2) 다른 쓰레드 : A, B pop, 이후 A push
(시간)
C
Head
A
(3) pop 중인 쓰레드 : head.CAS( A B ) 함수 실행
주소 A를 delete 한 이후, new로 A를 다시 할당 가능
B??
메모리 해제 기법 #2
• 해저드 포인터
–잠재적인 위험을 가진 포인터를 관리하는 기법
– 에서 특허 소유
해저드 포인터 설정
Pop (CAS)
해저드 포인터 해제
해저드 포인터 체크
지금 삭제 나중에 삭제
테이블 체크 & 삭제
Step 1 Step 2
Double-word CAS
• std::shared_ptr<T>는 double-word
– double-word CAS를 지원하면 해결됨
–다행히 최신 CPU 에서는 128-bit CAS 지원
• CPU에서 지원되지 않으면?
–대안 : 포인터 압축 테크닉
– 64bit 포인터의 일부를 다른 목적으로 사용
Double-word CAS
• 컴파일러의 std::atomic<T> 지원 여부
– is_lock_free()는 128bit 에서 false
– std::shared_ptr<T> 는 not trivially copyable 라서 atomic에 사용불가
• 컴파일러에서 지원되지 않으면?
–대안 : 전부 직접 구현
Split Reference Count
• external count
– 초기값 : 0
– pop 시도 전 증가
• internal count
– 초기값 : 0
– pop 시도 후 1 감소
node
data
internal_count
counted_node_ptr
counted_node_ptr
external_count
ptr
Split Reference Count
• pop() 성공 후
– Pop을 성공한 쓰레드는,
–최종 external_count 확보,
– internal_count 에 atomic_add
• pop() 시도 이후 (성공 / 실패 여부 상관없이)
– internal_count 감소 후 0면 노드 삭제
external_count +1 external_count +1
pop 성공 pop 실패
internal_count -1
internal_count +n
Split Reference Count
• 삭제 지연 Case 1
< 0
= 0, delete
external_count +1 external_count +1
pop 성공 pop 실패
internal_count +n
internal_count -1
Split Reference Count
• 삭제 지연 Case 2
= 0, delete
> 0
Lock Free Stack 정리
• CAS를 이용해서 구현
• 어려운 부분 : 메모리 해제 문제
–몰아서 해제
–해저드 포인터
–레퍼런스 카운팅
• Double-word CAS 지원 문제
• std::shared_ptr의 atomic 지원 문제
• 직접 구현
기타 최적화
• Atomic operation 메모리 모델 최적화
–아직까지는 Memory model 고려하지 않음
–기본 값 : memory_order_seq_cst 적절한 메모리 모델로 변경
• _InterlockedCompareExchange128
–메모리 모델 설정 불가
Queue 의 구분
• Single-producer (SP)
• Multi-producer (MP)
• Single-consumer (SC)
• Multi-consumer (MC)
• 제약 조건에 따라서
– SPSC, SPMC, MPSC, MPMC 로 구분
이건 쉬움
더미 노드를 사용하는 Queue
• push()
–더미 노드에 데이터만 추가 실제 node가 됨
–이후 새로운 더미 노드 생성
Head를 수정 할 필요가 없음
/ C
Tail
B
Head
A
Multi-consumer pop()
• Head != Tail 체크 후 CAS로 pop 시도 반복
• 메모리 해제 문제
–스택 구현에서 이미 다룬 문제
(책에서는 스택에서 구현했던 Split Reference Count로 진행)
Split Reference Count
• external count
– 최대 2개
• internal count
• external_counters
counted_node_ptr
external_count
ptr
counted_node_ptr
external_count
ptr
node
data
internal_count
counted_node_ptr
ext_counters = 2
문제점 #2 : Blocking
• Push 첫번째 단계...
–한 쓰레드가 데이터만 추가 한 상태에서,
–다른 쓰레드에서 첫번째 단계만 반복하면?
• 계속 실패, lock-free가 아니게 됨
D C
Tail
B
Head
A
Blocking 문제 해결
• Tail node에 데이터가 추가 되었을 때
–모든 쓰레드가 새 dummy node 추가 시도
–모든 쓰레드가 새 노드로 Tail 변경 시도
책을 보시면 됩니다. (어려우니 각오하시고..)
Lock Free Queue 정리
• CAS를 이용해서 구현
• 어려운 부분 : 메모리 해제 문제
–스택과 근본적으로는 같은 문제
– Tail 처리 구현 중 추가로 파생되는 문제
• ABA 문제
• Blocking ( busy-waiting ) 문제
총정리
• Lock-free 자료구조 작성 요령
–처음부터 메모리 모델을 고려하지는 않음
• 이미 충분히 어려우니까
– Lock-free 메모리 해제 기법들 적용
• stack 구현에서 제시한 세 가지 기법
– ABA 문제에 대한 고려 필요
– Busy-waiting을 인지하고 다른 쓰레드 일 해주기
• 누가 시작했던 멈추면 안됨