일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- jvm이란
- jvm 작동 원리
- 안드로이드 레트로핏 사용법
- rxjava hot observable
- rxjava disposable
- 2022 플러터 안드로이드 스튜디오
- 자바 다형성
- rxjava cold observable
- 객체
- 안드로이드 유닛 테스트 예시
- 안드로이드 유닛테스트란
- android retrofit login
- 안드로이드 라이선스
- 멤버변수
- 서비스 쓰레드 차이
- 스택 자바 코드
- ANR이란
- 클래스
- android ar 개발
- 스택 큐 차이
- 큐 자바 코드
- 플러터 설치 2022
- 2022 플러터 설치
- 서비스 vs 쓰레드
- ar vr 차이
- 안드로이드 os 구조
- 안드로이드 유닛 테스트
- Rxjava Observable
- 안드로이드 레트로핏 crud
- 안드로이드 라이선스 종류
- Today
- Total
나만을 위한 블로그
[Kotlin] 멀티 쓰레딩이란? synchronized란? 본문
예전에 synchronized를 주제로 자바 예시 코드를 사용한 포스팅을 작성했었다.
https://onlyfor-me-blog.tistory.com/251
지금 와서 다시 보니 설명이 빈약하고 코틀린으로 동작하는 코드로 다시 써보는 것도 좋겠다 싶어 다시 작성한다.
synchronized가 뭔지 알아보기 위해 검색하면 나타나는 키워드가 멀티 쓰레딩, 동시성 프로그래밍이다. 그러면서 프로세스와 쓰레드의 차이, 컨텍스트 스위칭이란 용어들도 나오는데 이것은 전에 작성한 포스팅을 남겨둔다. 또는 각자 알아서 확인하고 온다.
https://onlyfor-me-blog.tistory.com/390
멀티 쓰레딩이 무엇인지만 확인하고 synchronized를 확인한다. 아래는 영문 위키백과에서 말하는 멀티 쓰레딩이다.
https://en.wikipedia.org/wiki/Multithreading_(computer_architecture)
컴퓨터 아키텍처에서 멀티 쓰레딩은 CPU 또는 멀티 코어 프로세서의 단일 코어가 OS에서 지원하는 여러 쓰레드 실행을 동시에 제공하는 기능이다. 이 접근 방식은 다중 처리와 다르다. 멀티 쓰레드 앱에서 쓰레드는 컴퓨팅 장치, CPU 캐시와 TLB(변환 참조 버퍼)를 포함하는 싱글 or 멀티 코어의 리소스를 공유한다. 멀티 프로세싱 시스템이 하나 이상의 코어에 여러 완전한 처리 장치를 포함하는 경우, 멀티 쓰레딩은 명령 수준 병렬 처리뿐 아니라 쓰레드 수준 병렬 처리를 사용해서 단일 코어의 활용도를 높이는 것을 목표로 한다. 두 기술은 상호보완적이므로 다중 멀티쓰레딩 CPU와 다중 멀티쓰레딩 코어가 있는 CPU가 있는 거의 모든 최신 시스템 아키텍처에 결합된다
멀티쓰레딩 패러다임은 90년대 후반부터 명령어 수준 병렬성을 더 활용하려는 노력이 정체되면서 대중화됐다. 이를 통해 처리량(throughput) 컴퓨팅 개념이 더 전문화된 트랜잭션 처리 분야에서 다시 등장할 수 있었다. 싱글 쓰레드나 단일 프로그램의 속도를 더 높이는 것은 어렵지만 대부분 컴퓨터 시스템은 실제로 여러 쓰레드나 프로그램 간에 멀티 태스킹을 수행한다. 따라서 모든 작업의 처리량을 향상시키는 기술은 전반적인 성능 향상으로 이어진다. 처리량 컴퓨팅을 위한 2가지 주요 기술은 멀티 쓰레딩, 멀티 프로세싱이다
- 장점 : 쓰레드가 캐시 누락(cache misses)을 많이 받으면 다른 쓰레드는 쓰지 않은 컴퓨팅 리소스를 계속 활용할 수 있으며, 이는 싱글 쓰레드만 실행된 경우 이런 리소스가 유휴 상태였기 때문에 전체 실행 속도가 더 빨라질 수 있다. 또한 쓰레드가 CPU의 모든 컴퓨팅 리소스를 쓸 수 없는 경우, 다른 쓰레드를 실행하면 해당 리소스가 유휴 상태가 되는 걸 방지할 수 있다
- 단점 : 캐시나 TLB 같은 하드웨어 리소스를 공유할 때 여러 쓰레드가 서로 간섭할 수 있다. 결과적으로 싱글 쓰레드의 실행 시간은 향상되지 않고 쓰레드 전환 하드웨어를 수용하는 데 필요한 더 낮은 주파수 or 추가 파이프라인 단계로 인해 단 하나의 쓰레드만 실행되더라도 성능이 저하될 수 있다...(중략)...쓰레드 스케줄링도 멀티 쓰레딩의 주요 문제다
잘 모르는 용어들이 많지만 쓰레드 스케줄링이란 용어가 눈에 들어온다. 이건 무슨 용어인가?
https://en.wikipedia.org/wiki/Scheduling_(computing)
컴퓨팅에서 스케줄링은 작업 수행을 위해 리소스를 할당하는 작업이다. 리소스는 프로세서, 네트워크 링크, 확장 카드일 수 있다. 작업은 쓰레드, 프로세스, 데이터 흐름일 수 있다. 스케줄링 활동은 스케줄러라는 프로세스에 의해 수행된다. 스케줄러는 로드 밸런싱과 같이 모든 컴퓨터 리소스를 사용 상태로 유지하고, 여러 사용자가 시스템 리소스를 효과적으로 공유하거나 목표 서비스 품질을 달성할 수 있게 설계되는 경우가 많다. 스케줄링은 계산(computation)의 기본이며, 컴퓨터 시스템 실행 모델의 본질적 부분이다. 스케줄링은 CPU로 컴퓨터 멀티태스킹을 가능하게 한다...(중략)
https://www.iitk.ac.in/esc101/05Aug/tutorial/essential/threads/priority.html
- 많은 컴퓨터에는 CPU가 하나만 잇으므로 쓰레드는 다른 쓰레드와 CPU를 공유해야 한다. 하나의 CPU에서 여러 쓰레드를 어떤 순서로 실행하는 걸 스케줄링이라고 한다
- 각 쓰레드에는 MIN_PRIORITY, MAX_PRIORITY 사이의 숫자 우선순위가 있다. 언제든 여러 쓰레드를 실행할 준비가 되면 우선순위가 가장 높은 쓰레드가 선택된다. 해당 쓰레드가 정지하거나 일시 중지된 겨우에만 우선 순위가 낮은 쓰레드가 실행된다
- 시스템의 모든 Runnable 쓰레드가 같은 우선순위를 갖는다면, 스케줄러는 임의로 쓰레드 중 하나를 실행한다
- 특정 쓰레드는 언제든 yield()를 호출해 실행 권한을 포기할 수 있다. 쓰레드는 같은 우선순위를 가진 다른 쓰레드에만 CPU를 양보할 수 있다. 우선순위가 낮은 쓰레드에 양보하려는 시도는 무시된다
즉 쓰레드 스케줄링은 한 CPU에서 여러 쓰레드가 어떤 순서대로 실행될 수 있게 하는 작업이라는 것이다.
본론으로 돌아와서, 멀티 쓰레딩에 대해 정리하면 아래와 같다.
- 멀티 쓰레딩은 CPU가 동시에 여러 쓰레드를 돌릴 수 있게 해 주는 기능이다
- 전체적인 실행 속도가 향상될 수 있지만 캐시 등 하드웨어 리소스를 공유할 때 다른 쓰레드의 간섭으로 인해 하나의 쓰레드만 실행되어도 속도가 느릴 수 있다
다른 쓰레드가 간섭할 수도 있는 상황이 있다는 걸 알았으니, 이것을 막는 방법도 있을 것이다.
코틀린에서 사용할 수 있는 방법들은 대략 아래와 같다.
- 세마포어
- 뮤텍스
- Volatile
- Atomic 변수
- concurrent 컬렉션
- synchronized
마지막에 synchronized가 있는 걸 볼 수 있다. 즉 synchronized는 어떤 쓰레드가 리소스를 사용 중인 동안엔 다른 쓰레드들이 간섭하지 못하게 막아주는 역할을 하는 요소다. 좀 더 자세하게 말하면 어떤 쓰레드가 리소스를 사용하는 동안 다른 쓰레드가 해당 리소스에 접근하지 못하게 하는 락(Lock)을 제공한다.
이런 특징으로 멀티 쓰레딩을 다루는 코드에선 자주 등장하는 요소다. 코틀린에서의 synchronized 사용법은 아래와 같다.
fun main() {
val counter = Counter()
val threads = List(10) {
Thread {
repeat(1000) {
counter.increment()
}
}
}
threads.forEach { it.start() }
threads.forEach { it.join() }
println("getCount() : ${counter.getCount()}")
}
class Counter {
private var count = 0
fun increment() {
synchronized(this) {
count++
}
}
fun getCount(): Int {
synchronized(this) {
return count
}
}
}
// >> getCount() : 10000
Counter 클래스의 increment()를 10개의 쓰레드들이 동시 호출해서 count 변수를 증가시키는 예시 코드다.
synchronized(this) 블록은 this가 가리키는 객체에 대한 락을 획득한다. 이 락으로 인해 한 번에 1개의 쓰레드만 count 변수를 증가시킬 수 있게 되고, 그 동안 다른 쓰레드는 절대 count 변수에 접근할 수 없다.
총 10개의 쓰레드들이 생성되고 각 쓰레드들은 1,000번씩 count 변수를 1 증가시키기 때문에 10 * 1,000 = 10,000이 최종 결과로 출력돼야 한다. 그리고 실제로 콘솔에 그렇게 출력된다.
forEach를 2번 사용한 이유는 하나의 forEach 안에서 start() 후 join()을 호출할 경우, 각 쓰레드가 순차적으로 실행되어 병렬 처리를 하는 의미가 없기 때문이다. 무슨 뜻이냐면 1번째 forEach는 모든 쓰레드를 시작시키고 2번째 forEach는 각 쓰레드가 종료될 때까지 대기한다. start()는 비동기적으로 실행되기 때문에 start() 호출 후 threads 리스트 안의 다른 쓰레드가 start()를 호출한다. 이 과정은 매우 빠르게 이뤄지기 때문에 사실상 거의 동시에 쓰레드 10개가 시작된다고 볼 수 있다.
이후 2번째 forEach에서 10개의 쓰레드들이 모두 완료될 때까지 메인 쓰레드가 대기한다. 이후 10,000이라는 결과를 얻게 된다.
그런데 이렇게 하지 않고 하나의 forEach 안에서 start(), join()을 모두 호출한다면 각 쓰레드가 순차적으로 실행되기 때문에 병렬 실행으로 얻는 이점을 잃게 되는 것이다. 실제로 하나의 forEach에서 start(), join()을 모두 호출해 보면 콘솔엔 똑같이 10,000이 출력된다. 두 방식의 결과는 같지만 내부적으로 작동하는 방식이 다른 것이다. 지금이야 숫자만 1 증가시킬 뿐인 예시 코드지만, 다른 경우라면 어떤 결과가 나올지는 생각해 보면 금방 알 수 있을 것이다.
synchronized를 사용한 예시는 위의 코드 말고도 검색하면 많이 존재한다. 다른 코드들도 많이 보면서 멀티 쓰레딩과 synchronized 사용법을 알아두면 언젠가 쓸모있을 것이다.
'개인 공부 > Kotlin' 카테고리의 다른 글
[Kotlin] val은 Immutable이 아니다? (0) | 2024.11.17 |
---|---|
[Kotlin] 컬렉션 필터링 (filter, any, none, all) (0) | 2023.11.08 |
[Kotlin] KDoc 주석이란? (0) | 2023.09.19 |
[Kotlin] 함수형(SAM) 인터페이스란? (0) | 2023.09.19 |
[Kotlin] 에러 처리 Best Practices (0) | 2023.09.08 |