반응형
Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 안드로이드강좌
- Coroutine
- 스레드
- 디자인패턴
- Rxjava
- 안드로이드
- android
- 자바
- 알고리즘
- theming
- ReactiveProgramming
- 병렬프로그래밍
- kotlin강좌
- 알게되는
- 글또
- 코루틴
- 병럴프로그래밍
- viewmodel
- 커스텀상태
- 안드로이드스튜디오
- mockito
- 책
- 테스트
- g 단위테스트
- Gradle
- Compose
- k8s
- 코틀린
- Kotlin
- 회고
Archives
- Today
- Total
선생님, 개발을 잘하고 싶어요.
자바 병렬 프로그래밍 - 1부 5장 - 구성 단위 본문
- 자바 기본 라이브러리를 보면 병렬 프로그램 작성 시 필요한 여러 가지 동기화 도구가 마련되어있다.
- 스레드 안전한 컬렉션 클래스
- 동시 동작 스레드 간 작업 조율하는 동기화 클래스
동기화된 컬렉션 클래스
- Vector, Hashtable, Collections.synchronizedXxx
- 스레드 안정성 확보 방법 👉🏻 클래스 내부에 캡슐화 해 내부 값을 한 번에 한 스레드만 사용할 수 있도록 제어
동기화된 컬렉션 클래스의 문제점
- 클라이언트 코드에서 두 개 이상의 연산을 묶어서 사용하는 경우
- 반복, 컬렉션 내부의 모든 항목을 차례로 가져다 사용
- 이동, 특정한 순서에 맞춰 지금 항목의 다음 항목 위치로 이동
- 없는 경우에만 추가하는 기능
- 개별 연산에 대한 스레드 안전은 지켜주지만, 그 연산을 조합한 복합 연산은 적절한 lock을 클라이언트 코드가 호출해야함. 스레드 안전 켈렉션의 상태는 정상적인데 클라이언트 코드는 깨질 염려가 있음
- 스레드 안전 클래스가 제공하는 클라이언트 측 락을 사용해서 락을 걸어야함. (e.g. 컬렉션 클래스 자체)
- 이는 반복문도 마찬가지라서 반복문 전체를 락으로 감싸야함. 문제는? 반복문 실행되는 동안 다른 스레드는 해당 컬렉션에 일체 접근할 수 없기 때문에 병렬 프로그래밍의 장점을 잃는 셈이다.
Iterator와 ConcurrentModificationException
- 즉시 멈춤(fail-fast) 👉🏻 반복문을 실행하는 도중 컬렉션 클래스 내부의 값을 변경하는 상황 포착시 ConcurrentModificationException 예외 발생 및 멈추는 방법
- 멀티스레드 관련 오류가 있다는 경고 정도에 해당한다.
- 이를 미연에 방지하는 방법은 반복문 전체를 적절한 락으로 동기화 시키는 것 🤔
- 반복문 내부에서 다른 작업을 수행할 때 또 다른 락이 필요하다면 deadlock이 발생할 가능성도 높다.
- 이런 위험이 있는 상태에서 컬렉션 클래스를 오랜 시간 동안 락으로 맊는 건 전체 확장성을 해칠 수 있다.
- 컬렉션을 clone 하고 해당 로컬 복제본으로 반복문을 수행하는 식으로 우회로를 떠올릴 수 있다.
숨겨진 Iterator
- Collection.toString 과 같은 연산 내부에서 반복문(Iterator)를 내부적으로 사용하고 있다.
- 디버깅 메시지 출력하려 했을 뿐인데 ConcurrentModificationException가 발생할 수도 있다는 뜻 🤔
- 🌟 동기화 기법을 클래스 내부에 캡슐화해야 동기화 정책을 적용하기 쉽다.
병렬 컬렉션
- 동기화된 컬렉션 클래스는 내부 변수에 접근하는 통로를 일련화해서 스레드 안전성을 확보 한 것이라서 동시 사용성은 상당히 손해본다.
- 병렬 컬렉션 👉🏻 여러 스레드에서 동시에 사용할 수 있도록 설계했다.
- ConcurrentHashMap 👉🏻 HashMap 대치 및 병렬성 확보
- CopyOnWriteArrayList 👉🏻 추가된 객체 목록 반복 열람 연산의 성능을 최우선 구현한 List
- ConcurrentMap 인터페이스에는 putIfAbsent, replace, conditionalRemove 연산 등 자주 쓰이는 복합 연산을 단일 연산으로 내부적으로 제공하고 있다.
- 🌟 동기화 컬렉션을 병렬 컬렉션으로 교체만 해도 전체적인 성능을 상당히 끌어 올릴 수 있다.
ConcurrentHashMap
- 동기화 컬렉션과 다른 동기화 기법을 채택하면서 병렬성과 확장성이 나아졌다.
- 락 스트라이핑(lock striping, 11.4.3절)
- 다음과 같은 특성이 있다.
- 읽기 연산은 멀티 스레드 얼마든지 동시 처리
- 읽기와 쓰기 동시 처리
- 쓰기 연산 제한된 개수만큼 동시 처리
- 단일 스레드 환경에서도 성능상 손실이 거의 없음
- Iterator는 즉시 멈춤 대신 미약한 일관성 적략을 취한다.
- 미약한 일관성 전략 👉🏻 반복문 중 컬렉션 내용 변경해도 Iterator 만든 시점 상황대로 반복 계속 가능, 변경 내용 반영해 동작할 수도 있다(보장되진 않음).
- 병렬성을 위해 희생한 것
- size, isEmpty 메소드 의미가 약해짐, 추정 값일 뿐임
- get, put, containsKey, remove 등 핵심 연산 병렬성과 성능을 높이기 위한 선택
- 맵을 독점적으로 사용하도록 막는 기능 제공 ❌
Map 기반의 또 다른 단일 연산
- 독점적으로 사용할 수 있는 락이 없기 때문에 어라 개의 단일 연산을 모아 새로운 단일 연산을 만들고자 할 때 클라이언트 측 락 기법은 사용할 수 없다.
- 대신 putIfAbsent, removeIfEqual, replaceIfEqual 연산 등 자주 필요한 몇 가지의 연산을 미리 구현 제공한다.
CopyOnWriteArrayList
- 스레드 안정성 확보 방법 👉🏻 불벽 객체는 외부에 공개해도, 별다른 동기화 과정 없어도 스레드 안전, 컬렉션 내용 변경 마다 복사본 새로 생성해서 불변 객체 처럼 처리
- List에 들어 있는 값을 Iterator로 접근할 때 List 전체에 락을 걸거나 복제할 필요 ❌
- 변경할 때마다 복사라는 개념은 불변 객체를 외부에 공개하면 스레드 안전하다는 개념 도입했기 때문에 가능
- 주의 사항
- 데이터가 변경될 때마다 복사본을 만들기 때문에 성능 상 손해 가능
- 컬렉션에 데이터가 많다면 재생성에 큰 손실
- 🌟 변경 작업보다 반복문으로 읽어내는 일이 훨씬 빈번한 경우에 효과적
블로킹 큐와 프로듀서-컨슈머 패턴
- 🌟 BlockingQueue 핵심 메소드
- put 👉🏻 값을 추가할 공간이 있을 때까지 블락
- take 👉🏻 뽑아낼 값이 있을 때까지 블락
- offer 👉🏻 값을 추가할 때 공간이 없으면 오류 (대기하지 않음, 혹은 대기 시간 지정)
- pool 👉🏻 뽑아낼 값이 없으면 오류 (대기하지 않음, 혹은 대기 시간 지정)
- 프로듀서-컨슈머(producer-consumer) 패턴
- 해야 할 일 목록을 가운데에 두고 작업을 만들어 내는 주체와 작업을 처리하는 주체를 분리하는 설계 방법
- BlockingQueue를 사용하면 이 패턴의 해야 할 일 목록역할을 한다.
- 값이 들어올 때까지 take 메소드가 알아서 멈추고 대기하기 때문에 consumer 코드 작성이 쉽다.
- producer가 consumer가 감당하지 못할 만큼 일을 많이 만들지 않는 한, consumer는 다음 작업이 들어올 때까지 대기
- 큐의 크기에 제한을 두면 큐에 빈 공간이 생길 때까지 put 메소드가 대기하기 때문에 producer 코드 작성이 쉽다.
- offer를 사용하면 큐가 다 찼을 때 에러로 그 정보를 받아볼 수 있는데, 이를 통해서 producer가 작업을 많이 만들어 과부하에 이르는 상태를 좀더 효과적으로 처리할 수 있다.
- 부하 분배, 작업 내용 직렬화 및 임시 디스크 저장, producer 스레드 갯수 동적 조절 … 등으로 producer 작업 생산 양을 조정할 수 있다.
- 실제 구현체는
- LinkedBlockingQueue, ArrayBlockingQueue: FIFO 형태의 큐
- PriorityBlockingQueue: 우선 순위 기준 동작 큐
- SynchronousQueue: 큐에 항목을 쌓지 않고 producer가 consumer에게 직접 작업 전달
- 충분한 수의 consumer가 대기하고 있는 경우 사용하기 좋다.
직렬 스레드 한정
- 객체의 소유권을 producer → consumer 로 넘기는 과정에서 직렬 스레드 한정(serial thread confinement) 기법 사용
- 스레드 한정 객체는 특정 스레드만 소유권을 가진다.
- 객체를 안전 공개시 객체에 대한 소유권을 이전(transfer) 가능
- consumer가 객체에 대한 유일한 소유권 획득
- producer는 소유권 완전히 잃는다.
- 새로운 소유자 스레드는 객체 상태를 완벽하게 볼 수 있다.
- 원래 소유권을 갖고 있던 스레드는 전혀 상태를 알수 없게 된다.
- 객체 풀(object pool)은 직렬 스레드 한정 기법 잘 활용하는 예
- 객체의 소유권을 빌려주고 (안전하게 공개)
- 풀에 반납
- 가변 객체의 소유권을 이전해야 한다면?
Deque, 작업 가로채기(work stealing)
- BlockingDeque 인터페이스 존재 (기능은 우리가 아는 그 Deque + Blocking)
- 작업 가로채기 패턴
- 모든 consumer가 각자의 덱을 가진다.
- 자신의 덱에 들어있는 작업을 다 처리하면, 다른 consumer 덱에 쌓인 작업중 맨 뒤에 추가된 작업을 가로챈다.
- 규모가 큰 시스템 구현 적합
- 다른 consumer 작업을 가져올 때도 맨 뒤에서 작업을 가져오니 원래 소유자와 경쟁이 일어나지 않는다.
- 자신의 덱이 비었다고 쉬는 스레드가 없도록 관리한다.
블로킹 메소드, 인터럽터블 메소드
- 스레드는 다양한 이유로 블록 당하거나, 멈춰질 수 있다. (block, interrupt)
- 블로킹 연산 👉🏻 멈춘 상태에서 특정한 신호를 받아야 계속해서 실행할 수 있는 연산
- InterruptedException을 발생시키는 메소드는 블로킹 메소드라는 의미
- 인터럽트는 스레드가 서로 협력 실행하기 위한 방법
- 다른 스레드에게 실행을 멈추라고 요청할 뿐 멈추도록 강제 가능 ❌
- 일반적으로 특정 작업을 중간에 멈추게 하려는 경우에 사용
동기화 클래스
- 동기화 클래스(synchronizer) 👉🏻 상태 정보를 활용, 스레드 간 작업 흐름 조절을 위한 클래스
- 블로킹 큐, 세마포어, 배리어, 래치 …
- 동기화 클래스 구조적 특징
- 스레드가 언제 통과, 대기 하는지 결정하는 상태 정보 관리
- 상태 변경 메소드 제공
- 특정 상태에 진입할 때까지 효과적으로 대기할 수 있는 메소드 제공
- 🌟 어떤 조건을 만족할 때까지 대기하고 싶으면? 동기화 클래스를 살펴보자.
래치 (latch)
- 래치(latch) 👉🏻 terminal 상태에 이를 때까지 스레드가 멈추도록 해주는 동기화 클래스
- 스레드의 관문으로 생각할 수 있다. 래치가 terminal 상태에 다다르면 관문이 열리고 모든 스레드가 통과한다.
- 특정한 단일 동작 완료 전에 어떤 기능도 동작하지 않도록 막아야 하는 경우 유용
- 특정 자원 확보 전에는 작업을 시작하지 말아야 하는 경우
- 의존성 가진 다른 서비스가 시작하기 전에는 특정 서비스가 실행되지 않도록 막아야 하는 경우
- 특정 작업에 필요한 모든 객체가 준비될 때까지 기다리는 경우
FutureTask
- 반환 값을 가지는 Callback 인터페이스를 구현 해야함
- 총 세가지 상태를 가짐 시작 전 대기, 시작됨, 종료됨
- 실제로 연산을 실행한 스레드에서 만든 객체를 안전한 공개 방법을 통해서 실행시킨 스레드에게 넘겨준다.
- 다음 상황에 유용
- 비동기적인 작업을 실행하고자 할 때
- 시간이 많이 필요한 모든 작업에 실제 결과가 필요한 시점 이전에 미리 작업해 두는 용도
- 사용 방법 시점에
- 테스크를 시작해야한다.
- get 호출 시 로드 중이면 블락
- 이미 결과를 받아온 상태면 바로 반환
세마포어 (semaphore)
- 카운팅 세마포어는 특정 자원, 연산을 동시에 사용하거나 호출할 수 있는 스레드 수 제한시 사용
- 풀이나 컬렉션 크기에 제한을 두고자 할 때 유용
- 내부에 가상의 퍼밋(permit)을 만들어 상태 관리
- 외부 스레드는
- acquire 👉🏻 퍼밋을 요청해 확보 (남는 퍼밋이 생길 때 까지 대기)
- release 👉🏻 퍼밋을 반납
- 이진 세마포어는 비재진입 락 역할 하는 뮤텍스로 활용가능
배리어 (barrier)
- 배리어(barrier) 👉🏻 다른 스레드를 기다리 위한 동기화 클래스
- 특정 이벤트가 발생할 때까지 여러 스레드를 대기 상태로 잡아둘 수 있다.
- 모든 스레드가 배리어 위치에 이르러야 관문이 열리고 계속 실행할 수 있다.
- 동작은 다음처럼
- 스레드 각자가 배리어 포인트에 다다르면 await
- await 메소드는 모든 스레드가 배리어 포인트에 도달할 때까지 대기
- 실제 작업 병렬 처리하고, 다음 단계로 넘어가기 위해선 이전 단계의 모든 계산이 끝나야 할 때, 이전 단계 내용을 취합할 때 유용하다.
'일상 > 책 리뷰' 카테고리의 다른 글
자바 병렬 프로그래밍 - 2부 7장 - 중단 및 종료 (0) | 2022.06.19 |
---|---|
자바 병렬 프로그래밍 - 2부 6장 - 작업 실행 (0) | 2022.06.17 |
자바 병렬 프로그래밍 - 1부 4장 - 객체 구성 (0) | 2022.06.11 |
자바 병렬 프로그래밍 - 1부 3장 - 객체 공유 (0) | 2022.06.10 |
자바 병렬 프로그래밍 - 1부 2장 - 스레드 안정성 (0) | 2022.06.09 |
Comments