업데이트:

카테고리:

/

태그: , , ,

안드로이드에서 Thread Safe

  • SimpleDateFormat

    동기화적이지 않기 때문에 여러 스레드에서 호출할 경우 값이 달라질 수 있음

  • Singleton

    repository 나 room은 실행 중 하나만 있으면 되므로 이미 존재하면 생성된 인스턴스를 반환하면 된다.

    1. 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 AThread B 가 있을 때, Thread A 가 instance의 생성을 완료 하기전에 메모리에 할당이 가능하기 때문에 Thread B 가 할당된 것으로 보고 instance를 사용하려 했으나, 아직 초기화가 끝난 상태가 아니기 때문에 오류가 발생할 수 있다.

    2. 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 프로세스를 안드로이드 시스템이 시작될 때 실행하고 있어 앱을 실행시키는 데 필요한 시간을 단축시킨다.

스레드 간에 정보를 전달하는 방법, 프로그램의 모든 스레드는 같은 주소에 접근할 수 있다.

스레드가 지역변수에 데이터를 저장하면 다른 스레드는 지역변수에 접근할 수 없지만,공유 메모리에 데이터를 저장하여 다른 스레드와 통신 및 공유 작업에 변수를 사용할 수 있다.

  • 인스턴스 멤버 변수
  • 클래스 멤버 변수
  • 메서드 안에 선언된 객체
  1. 시그널링

    공유 메모리에 저장된 상태 변수를 통해 통신하는 동안, 스레드에서는 상태의 변화를 가져오기 위해 상태 변수를 polling 할 수 있다. 하지만 더 좋은건 상태가 변화되었을 때 다른 스레드에 알리는 기능인 시그널링이 있다.

  2. 블로킹 큐

    생산자 - 소비자 동기화 문제 해결 → [생산자 스레드 - 블로킹 큐 - 소비자 스레드]

    블로킹 큐는 생산자와 소비자 사이의 조정자 역할을 함

    생산자 → 데이터 메세지

    블로킹 큐 → 생산량 > 소비량, 새로운 메세지를 큐에 넣을 수 없도록 함

    소비자 → 큐에 넣어진 순서대로 메세지를 처리

  3. 안드로이드 메세지 전달

    안드로이드에서 위의 두 경우를 사용할 수 있지만 UI 스레드가 차단될 수도 있다.

    [UI스레드 → 처리해야 할 데이터 메세지 → 백그라운드 스레드] 작업을 처리하는 동안 발생하는 태스크를 없앨 수 있음

    Untitled (9)

    • [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와 바인딩 된다.

      1. 백그라운드 스레드에서 UI 업데이트 요청
      2. 메인 스레드에서 다음 작업 예약
      3. 반복 갱신 - UI를 일정 주기마다 갱신해야되는 경우
      4. 시간 제한 - 뒤로가기 버튼을 연속 2번 눌러야 나가지는 경우