kotlin lateinit, lazy by

개발/안드로이드 2021. 6. 5. 14:59 Posted by 아는 개발자

자바에서 흔히 보게 되는 NullPointerException 문제를 예방하고자 코틀린에서는 변수 선언에서부터 Nullable 변수의 선언부터 엄격하게 관리한다. 변수를 선언 할 때도 Nullable인지 아닌지를 구분해야하고 Nullable인 경우에는 변수를 호출하는 코드에서 Nullsafe 지시자를 표시해야하며 그렇지 않으면 컴파일 단계에서 에러를 발생시킨다.

 

var name: String? = null 
name = "abcd"
name?.length() // name이 여전히 null 일 가능성이 존재하므로, null safe 접근만 허용된다

var name2: String = "abcd"
name2 = null // name2가 nullable하지 않으므로 이 코드는 컴파일 오류가 발생한다

 

 

그런데 코드 상에서는 null이 될 수도 있지만 실제 동작 중에는 null이 될 소지가 없는 경우가 있다. 아래 코드처럼 전역 변수인데 실행과 동시에 초기화를 시키는 경우가 이렇다. 논리적으로는 안전한 코드임에도 불구하고 값에 접근 할 때 null safe 지시자를 표시해야하는 불편함이 생긴다. 

 

class TestActivity: Activity() {
    var name: String? = null
    override fun onCreate() {
        name = "abcd"
        print("${name?.length}")
    }
}

 

lateinit 

 

lateinit을 사용하면 변수의 값을 지정하는 작업을 뒤로 미룰 수 있다. Nullable 하지 않은 변수를 선언하면서 Assign 하는 작업을 뒤로 미루고 싶을때는 lateinit 키워드를 사용하면 가능하면 된다. 아래 코드는 name 변수 앞에 lateinit 키워드를 두고 onCreate 콜백에서 값을 바로 지정했다. 선언 당시 Non-Null String으로 선언했기 때문에 호출할 때 

 

class TestActivity: Activity() {
    lateinit var name: String
    override fun onCreate() {
        name = "abcd"
        print("${name.length}")
    }
}

 

lateinit은 mutable 변수만 가능하기 때문에 var 키워드를 가진 변수에서만 사용이 가능하다. 실행 중에 값을 변경할 필요가 있는 경우 유용하다. 그리고 만약 값을 assign하지 않고 변수 값을 호출하는 경우에는 Kotlin 언어 상에서 에러를 발생시킨다.

 

by lazy 

 

by lazy 키워드는 lateinit과 비슷하게 값을 지정하는 작업을 미루는 작업인데 assign 되는 시점이 변수를 호출하는 시점이다. 아래 코드를 보면 name 변수 선언에 by lazy 키워드가 붙고 내부 브래킷에 "abcd" 코드가 있다. name변수가 호출되는 시점에 "abcd"로 assign 하겠다는 의미다. 실제 코드를 동작시켜보면 name 호출 시점에 by lazy 내부 로그가 먼저 호출되는 것을 볼 수 있다.

 

class Test {
    val name : String by lazy {
        println("this is name by lazy")
        "abcd"
    }

    init {
        println("I am here")
        println(name)
    }
}

// 실행 결과
I am here
this is name by lazy
abcd

 

by lazy는 immutable 변수에서만 적용이 가능해, val 키워드 변수에만 적용이 가능하다. 변수 값을 최초에만 설정하고 변경할 필요가 없는 경우 사용하면 유용하다. 변수 선언시에 값을 assign할 순 없지만 다른 변수들을 조합해 값을 설정하고 싶을 때 사용하면 유용하다.

728x90

멀티쓰레드 동시성

기술/컴퓨터사이언스 2021. 6. 5. 14:06 Posted by 아는 개발자

멀티 쓰레드 환경은 하나의 프로세스 내에 여러 개의 쓰레드가 동작할 수 있는 환경을 말한다. 쓰레드는 고유의 작업을 하면서 쓰레드 내에서 할당된 변수 뿐만 아니라 프로세스 내에 있는 변수에도 접근 할 수 있는데 이때 여러 개의 쓰레드가 같은 데이터에 접근하는 경우 경우에 따라 동시성 문제가 발생할 수 있다. 

 

 

위 그림은 스레드A, B가 프로세스 내에 상주한 정수형 변수 a에 접근하는 경우다. 둘다 read 명령으로 접근 하기 때문에 변수의 값이 변할 염려가 없다. 쓰레드 A, B 모두 변수의 값을 3으로 읽어오기 때문에 예상하지 못한 값을 읽어오게 되는 경우는 없다. 이 경우에는 동시성 문제가 발생하지 않는다.

 

 

그런데 위 그림에선 쓰레드 A가 a의 값을 1만큼 더해주는 작업을 하는데 이때는 접근 순서에 따라서 쓰레드 B가 읽어오게 되는 값이 달라질 소지가 생긴다. 쓰레드 B 가 접근하기 전에 쓰레드 A가 2번 접근했었다면 쓰레드 B는 3이 아닌 5를 값으로 읽어오게 되기도 하고 동시에 접근을 한다면 쓰레드 A가 값을 업데이트 하는 도중 쓰레드 B가 읽어서 변화된 값을 읽지 못하게 되는 우려도 생긴다. 위처럼 여러 개의 쓰레드가 공유된 자원에 접근 할 때 데이터의 신뢰성을 보장받을 수 없는 경우 쓰레드 동시성(Concurrency) 문제라한다.

 

728x90

'기술 > 컴퓨터사이언스' 카테고리의 다른 글

멀티쓰레드 동시성  (0) 2021.06.05
프로스세와 스레드  (0) 2021.06.05
스핀락, 뮤텍스, 세마포어  (0) 2018.11.07
RCU (Read-Copy Update)  (0) 2018.10.30
Cgroup (Control Group)  (0) 2018.09.15
CPU pinning과 taskset  (0) 2018.08.27

프로스세와 스레드

기술/컴퓨터사이언스 2021. 6. 5. 13:42 Posted by 아는 개발자

 

프로세스 (Process)

 

프로세스는 운영체제에서 프로그램을 구성하는 기본 단위다. 현재 글을 쓰고 있는 크롬 웹브라우저, 스마트폰에서 사용중인 넷플릭스, 카카오 앱도 모두 운영체제내에선 프로세스 단위로 이뤄진다. 특정 프로그램의 경우 두개 이상의 프로세스로 이뤄지는 경우도 있으나 하나의 프로세스로 이뤄진 경우가 일반적이다.

 

프로세스는 운영체제 고유 스케줄링에 관리를 받는다. 우리가 스마트폰에서 구글 뮤직 앱으로 음악을 들으면서 카톡을 할 수 있는 것도 구글 뮤직 앱 프로세스와 카톡 프로세스가 운영체제 스케줄링에 의해 관리되기 때문이다. 프로그램이 프로세스의 형태로 이뤄진 것도 어찌보면 운영체제 내에서 동작을 관리하기 위함으로 생각할 수도 있다.

 

쓰레드 (Thread)

 

프로세스내에서 실행되는 작업 단위다. 프로세스의 데이터에 접근 할 수 있고 특정한 작업을 맡길 때 사용한다. 안드로이드의 경우를 예로 들면 대표적으로 화면을 담당하는 UI 쓰레드와 네트워크나 디스크 작업에 쓰이는 I/O 쓰레드가 있다. 각 쓰레드 모두 하나의 프로세스내에서 실행되며 서로 데이터를 공유 할 수 있다.

 

하나의 프로세스에 여러개의 스레드를 두는 경우 멀티 스레드 환경이라고 부르며 동시에 이뤄져야 하는 작업이 있는 경우 이 방식으로 소프트웨어를 구성한다. 예로들면 화면을 업데이트 하면서 파일을 다운받는 경우 하나의 스레드에서 작업한다면 둘 중 하나가 끝나야 다음 작업이 가능한데 각각 별도의 쓰레드를 둔다면 동시에 작업이 가능하다. 그래서 대부분의 프로그램이 멀티 쓰레드 방식으로 구현된다.

 

728x90

'기술 > 컴퓨터사이언스' 카테고리의 다른 글

멀티쓰레드 동시성  (0) 2021.06.05
프로스세와 스레드  (0) 2021.06.05
스핀락, 뮤텍스, 세마포어  (0) 2018.11.07
RCU (Read-Copy Update)  (0) 2018.10.30
Cgroup (Control Group)  (0) 2018.09.15
CPU pinning과 taskset  (0) 2018.08.27

메뉴 리뉴얼

알고리즘/프로그래머스 2021. 5. 29. 18:09 Posted by 아는 개발자

문제 푸는 키 포인트는 두개다.

 

1. 손님들마다 주문한 단품 메뉴 목록에서 가능한 조합을 목록으로 만든다.

 

손님이 최대 주문 할 수 있는 단품 메뉴의 개수가 10개다. 그러면 10개로 만들 수 있는 조합의 개수는 2^10 - 1개. 알고리즘 시험 문제에서 풀 수 있을 만큼의 범위다. 단품의 개수가 2개 이상인 조합만 고려대상이지만 이건 일단 무시하도록 하자.  코딩으로 조합 목록을 만드는 방법은 다양하게 있을텐데 내 경우에는 비트마스크를 사용했다. 만약 손님이 5개를 주문 했다면 총 31가지 조합이 만들어지므로, 1 ~ 31 까지 For 루프를 돌고 각 회차별로 비트를 확인 해서 단품 목록을 포함 시킬 것인지 말 것인지를 결정한다. 

 

for (int i = 0; i < orders.size(); i++) {

    int totalCombnation = (1 << (orders[i].length())) - 1;
    for (int bit = 1; bit <= totalCombnation; bit++) {
        vector<char> orderSet;
        string combi = "";
    
        for (int position = 0; (1 << position) <= bit; position++) {
            if (bit & (1 << position)) {
                orderSet.push_back((char) orders[i][position]);
            }
        }

        sort(orderSet.begin(), orderSet.end());

        for (int i = 0; i < orderSet.size(); i++) {
            combi += orderSet[i];
        }

 

totalCombination은 현재 주문에서 가능한 조합이 가능한 총 개수다. bit는 1에서부터 totalCombination 까지 순회하면서 단품 목록에서 조합이 가능한 쌍의 집합을 의미한다. 조합에 포함할지 말지는 position 변수가 있는 For 문에서 결정한다. 포함 유무는 orderSet 에 담고 오름차순으로 정리하고 문자열로 조합을 만들었다. 

 

2. 주문 조합 별로 노출 횟수 관리하기

 

모든 주문들의 조합을 순회하면서 노출된 횟수를 저장해야한다. 간단한 방법은 모든 주문 조합 별로 인덱싱이 가능한 int 배열을 만든 다음에 각각을 순회하면서 노출 횟수를 카운트 하는 것이다. 그런데 주문개수가 10개인 경우 알파벳 형태로 가능한 조합의 총 개수는 (26)*(25)*...(17) 이므로 배열에 둘 수 있는 구조가 아니다. 이럴때는 해시 자료구조를 쓴다. 문제에서 주어진 조건으로 가능한 모든 주문의 조합은 10 * 1024개이므로 해시 구조의 최적화를 이용한다면 메모리 범위 내에서 모든 자료구조를 담을 수 있다. 

 

map<string, int> combinations;
map<string, int>::iterator it = combinations.find(combi);

if (it == combinations.end()) {
    combinations.insert(make_pair(combi, 1));
} else {
    it->second++;
}

key와 value가 string, int인 해시 함수를 이용해서 주문 조합의 노출 횟수를 관리했다. 현재 주문 조합이 해시함수에 없으면 새로운 조합을 추가했고 있다면 노출 횟수를 +1 시켜주는 함수다. 이렇게 관리한 자료구조로 노출이 가장 많은 주문 조합을 고를 수 있다.

 

https://github.com/kwony/algorithm/blob/main/Programmers/72411.cpp

728x90

'알고리즘 > 프로그래머스' 카테고리의 다른 글

메뉴 리뉴얼  (0) 2021.05.29

Kotlin - Coroutine

개발/안드로이드 2021. 5. 21. 20:00 Posted by 아는 개발자

Coroutine을 공부할 때 당장 실행되는 코드를 짜려고 launch, async 함수부터 먼저 써보게 되는데(과거의 나) 이것보단 Coroutine을 이루는 구조가 무엇인지를 먼저 공부하고 유틸리티 함수를 사용하면 훨씬 이해하기가 쉽다.  Coroutine을 이루는 구조는 크게 CoroutineScope과 CoroutineContext다. 아래 그림으로 보면 CoroutineScope이 CoroutineContext를 포함하는 관계다.

 

 

1. CoroutineScope

 

CoroutineScope은 Coroutine이 활동할 수 있는 범위를 말한다. 예를 들어 Coroutine이 ViewModel의 생성주기 내에서만 동작하게 할 수 있고 Activity Lifecyle 생명주기를 따라서 동작하게 할 수 있는데 CoroutineScope은 Coroutine의 활동범위를 말한다. 이 속성을 잘 이용하면 Component의 생성주기에 맞춰 Coroutine 작업을 자동으로 취소할 수 있어서 유용하다. Kotlin에서는 안드로이드에서 사용할 수 있도록 몇가지 CoroutineScope을 미리 만들어뒀다. 

 

  • GlobalScope: 앱 프로세스의 생명주기를 따라감. 
  • MainScope: UI 관련 작업을 처리하는 용도.
  • ViewmodelScope: ViewModel의 생성주기를 따라감.
  • LifecycleScope: Activity, Fragment의 생명주기를 따라감. 생명주기별로 콜백이 다르다.
MainScope().launch {}

GlobalScope.launch {}

 

CoroutineScope 인터페이스를 구현해서 커스텀한 CoroutineScope을 만들수도 있긴 한데 공식 문서에서 이 방법은 추천하진 않고 있다.

 

2. CoroutineContext 

 

CoroutineContext는 Coroutine을 이루는 정보다. Coroutine 이름, Job, Dispatcher, ExceptionHandler 가 이에 해당한다. Dispatcher는 Coroutine이 실행될 쓰레드 풀을 의미한다. 대표적으로 Main, IO 쓰레드 풀이 있어서 UI 작업의 경우에는 Main, 디스크 작업에는 IO 쓰레드를 사용하도록 지정 할 수 있다. Executors 라이브러리를 이용해 커스텀으로 만든 쓰레드 풀에도 지정이 가능하다. 아래 코드는 우선순위가 높은 쓰레드 풀에서 동작하는 CoroutineScope을 만든 예다.

 

val customExecutor: Executor = Executors.newCachedThreadPool { r ->
    Thread(r, "CustomThread").apply {
        priority = Thread.MIN_PRIORITY
    }
}
val customDispatcher = object : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        customExecutor.execute(block)
    }
}

CoroutineScope(customDispatcher).launch {

}

 

ExceptionHandler는 Coroutine 내의 코드 실행중 발생하는 Exception을 처리할 수 있는 Handler다. 현재 Scope 별로 Exception Handler를 다르게 둘 수 있기 때문에 이것도 잘 써먹으면 유용하다. 

 

val handler = CoroutineExceptionHandler { context, th->
    println("$context ${th.toString()} ")
}

GlobalScope.launch(handler) {
    val async1 = async(){ 1 }
}

 

3. Utility 함수 

 

3.1 launch

 

launch 함수는 CoroutineScope내에서 실행되며, 현재 쓰레드를 막지 않고(blocking) 동작할 수 있는 새로운 Coroutine Job을 생성한다. 병렬로 수행되기 때문에 여러가지 작업을 동시에 수행할 때 쓰면 좋다. 아래 코드는 GlobalScope 내에서 두개의 Coroutine Job을 생성한 코드다. 앞에 코드에 300ms 의 딜레이를 줬다. 그 결과 scope2가 먼저 프린트 되고, scope1은 그 이후에 프린트 된다.

 

GlobalScope.launch {
    launch {
        delay(300)
        println("scope1")
    }
    launch {
        println("scope2")
    }
}

2021-05-21 16:14:17.677 I/System.out: scope2
2021-05-21 16:14:17.978 I/System.out: scope1

 

Job 내부 함수인 join() 은 동시성을 제어할 수 있는 함수다. 현재 Coroutine의 실행이 종료되지 않을 때까지 다음 코드를 실행하지 않는다. 순서를 관리할 때는 이 함수를 쓰면 된다. 그리고 cancel() 처럼 취소할 수 있는 함수도 있다. 이건 실제로 사용하다 보면 어떻게 써야하는지 감이 온다.

 

3.2 async 

 

async 함수는 launch 와 거의 동일하고 결과 값을 받을 수 있다는 점이 추가 됐다. 아래 코드의 두 async Job은 각각 1, 2를 리턴하는 CoroutineScope이다. async 내부 await() 함수는 여기서 실행된 결과 값을 받아오게 된다. 여기서 주의깊게 볼 부분은 각각에 delay를 300ms, 100ms 씩 줬는데도 start 로그로부터 결과 값까지 걸린 시간은 둘의 합인 400ms가 아니라 가장 긴 delay인 300ms라는 점이다. 두 Job을 병렬로 처리했기 때문에 가장 delay를 오래 잡는 Job의 시간만큼 소요된다.

 

GlobalScope.launch {
    println("start")
    val a = async {
        delay(300)
        1
    }
    val b = async {
        delay(100)
        2
    }

    println("a + b = ${a.await() + b.await()}")
}

2021-05-21 16:22:19.885 I/System.out: start
2021-05-21 16:22:20.190 I/System.out: a + b = 3

 

3.3 withContext 

 

동일한 CoroutineScope 내에서도 종종 Coroutine Context를 바꿔야 할 일이 생긴다. 예를 들면 I/O 작업을 수행 중에도 중간중간 화면 UI를 업데이트 해야하는 경우 Dispatcher를 바꿔 코드에 적용하는 쓰레드를 변경해야한다. 이럴때 쓰면 유용한 함수가 withContext다. 아래 코드는 I/O 스레드 풀에서 "abc", "def"라는 문자열을 받아오고 두 문자를 합해서 text라는 변수를 만들었다. UI 에 적용하려면 withContext를 이용해 임시로 Main함수로 바꿔주어 UI 컴포넌트에 접근 할 수 있다. 참고로 withContext는 내부적으로 async{}.await()로 구현돼 있어 내부 코드가 모두 실행된 다음에 다음 코드로 넘어가게된다. 

 

CoroutineScope(Dispatchers.IO).launch {
    val a = async {
        "abc"
    }
    val b = async {
        "def"
    }
    
    val text = a.await() + b.await()
    
    withContext(Dispatchers.Main) {
        textView.text = text
    }
}
728x90