Android Thread Safe
업데이트:
카테고리: Android
/안드로이드에서 Thread Safe
-
SimpleDateFormat
동기화적이지 않기 때문에 여러 스레드에서 호출할 경우 값이 달라질 수 있음
-
Singleton
repository 나 room은 실행 중 하나만 있으면 되므로 이미 존재하면 생성된 인스턴스를 반환하면 된다.
-
Double Checked Locking
class Singleton { private val instance : Singletone private fun Singleton() {} fun getInstance() { if (instance == null) { // 특정 스레드만 접근 가능하도록 설정, 나머지는 접근시 lock이 걸림 synchronized(Singleton::class.java) { if ( instance == null ) { instance = Singleton } } } return instance } }
instance가 null 인지 확인하여, null이라면 동기화 블럭에 진입화 초기화를 진행한다. 생성된 이후에는 동기화 블럭에 진입하지 않지만 아주 안좋은 확률로 정상 동작을 안 할 수 있다.
Thread A
와Thread B
가 있을 때,Thread A
가 instance의 생성을 완료 하기전에 메모리에 할당이 가능하기 때문에Thread B
가 할당된 것으로 보고 instance를 사용하려 했으나, 아직 초기화가 끝난 상태가 아니기 때문에 오류가 발생할 수 있다. -
Enum
// 숫자와 색깔이 어떤 값과 매치되는지 알고있어야함 class ColorInt{ static String getType(int type) throws IllegalArgumentException { if(type == 1){ return "RED"; }else if(type == 2){ return "BLUE"; }else if(type == 3){ return "GREEN"; }else{ throw new IllegalArgumentException(); } } } // 내부 객체를 만들어서 색깔을 표현하는 경우 class ObjectColor{ private ObjectColor(){} static final ObjectColor RED = new ObjectColor(); static final ObjectColor BLUE = new ObjectColor(); static final ObjectColor GREEN = new ObjectColor(); } enum Color{RED, BLUE, GREEN}
- Singleton 패턴을 적용할 때 유용, 한번만 생성됨
- Thread Safe하다, enum은 자동으로 synchronize하게 생성되기에 thread safe 하다.
스레드 안전하게 수행하기 위해서는
withLock
을 사용해서 여러 곳에서 한 자원을 동시에 접근하지 않도록한다.로직에서 한번에 여러 곳에서 동시에 값을 바꾸려고 시도하는 경우….
private val mutex = Mutex() // our hero ;-) private var counterWithMutex = 0 private var counterNoMutex = 0 suspend fun main( ) { val job1NoMutex = CoroutineScope(Default).launch { for (i in 1..500) { incrementCounterByTenNoMutex() } } val job2NoMutex = CoroutineScope(Default).launch { for (i in 1..500) { incrementCounterByTenNoMutex() } } val job3WithMutex = CoroutineScope(Default).launch { for (i in 1..500) { incrementCounterByTenWithMutex() } } val job4WithMutex = CoroutineScope(Default).launch { for (i in 1..500) { incrementCounterByTenWithMutex() } } joinAll(job1NoMutex, job2NoMutex, job3WithMutex, job4WithMutex) println(" No Mutex Tally: $counterNoMutex") println("With Mutex Tally: $counterWithMutex") } private suspend fun incrementCounterByTenWithMutex() { mutex.withLock { for (i in 0 until 10) { counterWithMutex++ } } } private fun incrementCounterByTenNoMutex() { for (i in 0 until 10) { counterNoMutex++ } }
-
안드로이드 멀티스레드
UI 스레드
ui에 관한 모든 업데이트를 처리하는 단일 스레드이다. 모든 클릭 핸들러
, 기타 ui
, 수명 주기 콜백
을 호출한다.
16ms 마다 화면을 업데이트해야 사용자에게 원할한 사용환경을 제공할 수 있다. 다른 스레드에서 작업을 할 때에는 ui스레드가 아무 작업도 하지 않게 해야됨
UI 스레드는 하나의 스레드만 접근가능하도록 한다.
바인더 스레드
서로 다른 프로세스에서 스레드 사이의 통신에 사용한다. 각 프로세스는 스레드 풀이라는 스레드의 집합을 유지하고, 이 스레드 풀은 종료되거나 재생성되지 않는다.
이러한 스레드는 시스템 서비스
, 인텐트
, 콘텐츠 프로바이더
, 서비스
… 다른 프로세스에서 들어오는 요청을 처리한다.
IO, 백그라운드 스레드
UI 스레드에서 파생된 스레드, Ui 스레드의 속성을 상속받는다.
Thread 제작
Thread는 직접 Thread를 상속받기보다는 Runnable
인터페이스를 상속받아서 사용한다.
받고싶지 않은 메소드는 빼고 상속받을 수 있어서 유리하다.
그리고 private 설정된 값도 받지 않을 수 있어 오버라이딩과, 쓸일없는 코드의 작성을 줄여 도움이된다.
파이프 사용 (운영체제 수준)
-
파이프란?
단방향 데이터 흐름을 가진다. 두 프로세스 사이에 일시적인 데이터 통로이다.
ex) window에서 마우스와 키보드의 입력이 일어나면 출력이 디스플레이로 보여지게 되고, 이 출력은 다른 프로세스의 입력으로 들어간다.
파이프는 선입선출의 원칙을 따른다. 첫 번째 프로세스의 결과물이 두 번째 프로세스의 입력이된다.
파이프의 생명주기는 [설정, 데이터 전송, 분리] 세 단계로 진행된다.
파이프가 가득 차면, 쓰기 스레드가 추가할 데이터를 위한 공간을 확보하기 전까지는 write
메서드는 차단된다. read
는 더이상 읽을 데이터가 없을 때 마다 차단된다.
파이프에 쓴 후 flush
메서드를 호출하면 새로운 데이터가 도착했음을 소비자 스레드에 알려준다. 보통 pipedReader는 1초 간격을 두고 wait
로 차단을 요청하기 때문에, flush
메서드를 호출하면 생산자 스레드는 소비자 스레드를 기다리는 시간을 단축하고 데이터를 처리할 수 있다.
write
를 닫으면 파이프는 분리되지만 데이터는 읽힐 수 있다. read
를 닫으면 버퍼가 지워진다.
공유 메모리
안드로이드는 Zygote
프로세스를 안드로이드 시스템이 시작될 때 실행하고 있어 앱을 실행시키는 데 필요한 시간을 단축시킨다.
스레드 간에 정보를 전달하는 방법, 프로그램의 모든 스레드는 같은 주소에 접근할 수 있다.
스레드가 지역변수에 데이터를 저장하면 다른 스레드는 지역변수에 접근할 수 없지만,공유 메모리에 데이터를 저장하여 다른 스레드와 통신 및 공유 작업에 변수를 사용할 수 있다.
- 인스턴스 멤버 변수
- 클래스 멤버 변수
- 메서드 안에 선언된 객체
-
시그널링
공유 메모리에 저장된 상태 변수를 통해 통신하는 동안, 스레드에서는 상태의 변화를 가져오기 위해 상태 변수를
polling
할 수 있다. 하지만 더 좋은건 상태가 변화되었을 때 다른 스레드에 알리는 기능인 시그널링이 있다. -
블로킹 큐
생산자 - 소비자 동기화 문제 해결
→ [생산자 스레드 - 블로킹 큐 - 소비자 스레드]블로킹 큐는 생산자와 소비자 사이의 조정자 역할을 함
생산자 → 데이터 메세지
블로킹 큐 → 생산량 > 소비량, 새로운 메세지를 큐에 넣을 수 없도록 함
소비자 → 큐에 넣어진 순서대로 메세지를 처리
-
안드로이드 메세지 전달
안드로이드에서 위의 두 경우를 사용할 수 있지만 UI 스레드가 차단될 수도 있다.
[UI스레드 → 처리해야 할 데이터 메세지 → 백그라운드 스레드] 작업을 처리하는 동안 발생하는 태스크를 없앨 수 있음
[Looper](https://developer.android.com/reference/android/os/Looper)
: Message Queue 를 관리, Message, Runnable 객체를 하나씩 꺼내서 Handler에 전달한다. 이렇게 관리하기 때문에 Race Condition을 방지할 수 있다.[Message Queue](https://developer.android.com/reference/android/os/MessageQueue)
: Message를 담는 Queue 형태의 자료 구조, 메세지를 처리하면 Queue에서 삭제하고, 메시지를 추가하면 Queue에도 추가해야 되기에 추가, 삭제가 용이해야된다. Message가 실행되는 순서대로 추가가 되며, 중간에 먼저 처리되어야 하는 Message가 들어오면 삽입이 될 수도 있다. (타임스탬프를 기준으로 정렬이 됨)Message
: 데이터나 태스크 중 하나만 옮기는 컨테이너 객체, 데이터는 소비자 스레드에서 처리되지만 태스크는 큐에서 나와 다른 처리해야 될것이 없을 때 실행 → 데이터가 있는 상태에서 실행된다면 처리해야되는 메세지들이 뒤로 밀리고 선입선출의 원칙이 깨지게 된다.- 데이터 메세지 -
Message.obtain(Handler, what, args1, args2, object)
- 태스크 메세지 -
Message.obtain(Handler, Runnable)
- 데이터 메세지 -
-
Handler
: Looper로 부터 처리되어야 하는 Message를 받아서 처리하는 역할 & Message Queue에 Message를 전달, 다른 스레드에서 요청하는 메세지를 처리하고 다시 해당 Thread에 메세지를 전달할 수도 있기에 스레드간의 통신을 담당한다.Looper가 없으면 message를 받을 수 없기때문에 인스턴스 생성 시점에 Looper와 바인딩 된다.
- 백그라운드 스레드에서 UI 업데이트 요청
- 메인 스레드에서 다음 작업 예약
- 반복 갱신 - UI를 일정 주기마다 갱신해야되는 경우
- 시간 제한 - 뒤로가기 버튼을 연속 2번 눌러야 나가지는 경우