
val a: List<Int> = listOf(1) // 읽기만 가능한 리스트
// a.add(2)
val b : MutableList<Int> = mutableListOf(1) // 읽기와 쓰기가 가능한 리스트
b.add(2)
listOf로 생성된 리스트는 불변 리스트 (읽기만 가능) -> List<T>mutableListOf로 생성된 리스트는 가변 리스트이다. (읽기와 쓰기가 가능) -> MutableList<T>Kotlin (그리고 Java)에서는 제네릭 타입 정보는 런타임에 지워진다. -> Type Erasure(타입 소거)
즉, 두 타입은 런타임에서 구분할 수 없음: List<Int>, List<String>
→ 둘 다 그냥 List로만 인식됨.
fun <T> checkType(param : T) {
if ( param is List<*> ) { // List<T> x !! 타입을 모르기 때문에 *를 써준다.
println("param 은 리스트 타입입니다.")
} else {
println("param은 List 타입이 아닙니다.")
}
}
is List<T>는 T가 런타임에 어떤 타입인지 모르기 때문에 검사 자체가 컴파일 에러가 뜬다.T는 컴파일 타임에만 존재하고, 런타임에서는 List만 존재하게 된다.비동기 프로그래밍을 가능하게 해주는 개념으로, 일반 함수처럼 보이지만 중간에 실행을 멈췄다가 다시 시작할 수 있는 함수이다. 즉, 일시 중단(suspend)과 재개(resume)가 가능한 함수
루틴은 정해진 작업을 수행하는 코드 블록을 말한다. 일반적으로 함수나 메서드 형태로 사용된다. 루틴은 보통 재사용성과 구조화된 프로그램 작성을 돕기 위해 사용된다.
코루틴은 스택 대신 힙을 사용해 상태를 저장한다.
suspend 키워드를 만나며 현재 상태를 힙에 저장하고 스레드를 반환한다.일시 중단 시 스레드를 차지하지 않는다.
delay, IO withContext(IO) 같은 suspend 지점을 만나면, 그래서, 스레드보다 코루틴이 가볍다고 하는 것이다.
class CoroutineThread() : Thread() {
override fun run() {
println("수행할 명령입니다! 현재 쓰레드 : ${Thread.currentThread().name}")
}
}
class CoroutineThread2: Runnable {
override fun run() {
println("수행할 명령입니다! 현재 쓰레드 : ${Thread.currentThread().name}")
}
}
val thread1: Thread = CoroutineThread()
thread1.start()
val thread2 = Thread(CoroutineThread2())
thread2.start()
val thread3 = Thread {
println("수행할 명령입니다! 현재 쓰레드 : ${Thread.currentThread().name}")
}
thread3.start()
val thread4 = object : Thread() {
override fun run() {
println("수행할 명령입니다! 현재 쓰레드 : ${Thread.currentThread().name}")
}
}
thread4.start()
// thread start = true로 하면 바로 실행된다.
thread(start = false, name="성찬이 쓰레드") {
println("수행할 명령입니다! 현재 쓰레드 : ${Thread.currentThread().name}")
}.start()
Thread() 클래스나 Runnable 인터페이스를 구현한 구현체를 이용해 쓰레드를 실행시킬 수 있다. thread(start=false, name = "{쓰레드 이름}") 방식으로도 쓰레드를 실행할 수 있다.val threadPool = Executors.newFixedThreadPool(5)
val services = List(100) { idx ->
{
println("작업 ${idx}번 시작 - 사용된 쓰레드 : ${Thread.currentThread().name}")
Thread.sleep(2000)
println("작업 ${idx}번 종료 - 사용된 쓰레드 : ${Thread.currentThread().name}")
}
}
for (logic in services) {
threadPool.submit { // 놀고 있는 스레드가 있다면 logic()을 실행하라고 하는 것
logic()
}
}
threadPool.submit을 이용하여 함수를 실행한다.threadPool.shutdown()
fun main() = runBlocking<Unit> {
runBlocking은 메인 쓰레드에서 코루틴을 시작하고, 코루틴이 모두 끝날 때까지 메인 쓰레드를 블로킹하게 된다. fun main() {
GlobalScope.launch {
println("현재 스코프 : ${this.coroutineContext}")
println("현재 쓰레드 : ${Thread.currentThread().name}")
// 현재 쓰레드 : DefaultDispatcher-worker-1
}
println("Hello, ")
Thread.sleep(3000) // 기다려줘야지만 GlobalScope 흐름이 실행되는 것을 볼 수 있다.
}
Dispatchers.Default가 사용되고,Dispatchers.Default는 CPU 코어 수만큼 미리 만들어진 스레드 풀(Worker Thread Pool)에서 실행된다. (이 스레드 풀은 코틀린 런타임이 관리한다.)GlobalScope
GlobalScope는 전역 코루틴 스코프로, app 전체에서 공통으로 사용하는 전역 생명주기를 따라가는 가장 넓은 스코프이다.GlobalScope는 DefaultContext를 가진다.launch
launch는 코루틴을 새로운 작업 단위(코루틴)로 실행하는 함수이다.launch는 즉시 실행되며, 결과값이 없고 작업이 끝날 때까지 기다리지 않아도 된다. (비동기).launch는 Job 객체를 반환해서, 나중에 작업을 취소(cancel())하거나 완료 대기(join())를 할 수 있다. GlobalScope.launch { // async는 뭔가 반환할 때 쓴다.
println("[2] GlobalScope.launch 진입 - 현재 쓰레드 : ${Thread.currentThread().name}")
repeat(3) {
delay(500)
println("[3] GlobalScope 내부 작업 처리중.. ${it}")
}
println("[4] GlobalScope.launch 종료")
}
println("[5] launch 호출 직후!")
}
Thread.sleep(10000)
GlobalScope은 main함수의 생명주기를 따라가기 때문에 Thread.sleep(10000)을 해주지 않으면 코루틴 내부의 동작을 하지 못하고 중단된다. [5] launch 호출 직후!가 '[2],[3],[4]' 보다 빨리 출력된다. threadPool.submit {
println("[1] 작업 시작 - 현재 쓰레드 : ${Thread.currentThread().name}")
runBlocking { // async는 뭔가 반환할 때 쓴다.
println("[2] runBlocking.launch 진입 - 현재 쓰레드 : ${Thread.currentThread().name}")
repeat(3) {
delay(500)
println("[3] runBlocking 내부 작업 처리중.. ${it}")
}
println("[4] runBlocking.launch 종료")
}
println("[5] launch 호출 직후!")
}
runBlocking은 내부 코드를 코루틴 스코프에서 동기적으로 실행하며, 실행이 끝날 때까지 현재 스레드를 블로킹한다.[1] ~ [5] 까지 순차적으로 출력되며, Thread.sleep()을 하지 않아도 로직이 다 실행되고 main 함수가 종료된다. val result: Job = GlobalScope.launch {
delay(1000L)
println("World!")
}
println("Hello,")
println("job.isActive : ${result.isActive}, completed: ${result.isCompleted}")
Thread.sleep(2000L)
println("job.isActive : ${result.isActive}, completed: ${result.isCompleted}")
launch는 Job을 반환하는데, 이 isActive, isCompleted 로 이 Job의 실행여부, 완료 여부를 확인할 수 있다.
fun main() = runBlocking{
val result1: Job = launch {
delay(500)
println("World!")
}
val result2: Job = launch {
delay(2500)
println("World!")
}
println("Hello,")
result1.join()
result2.join()
}
launch 두 개가 비동기적으로 실행된다.delay()를 걸고 멈춰 있다."Hello,"가 먼저 실행되고, 이후 join()으로 결과를 기다렸다가 결과를 출력하게 된다.fun main() = runBlocking<Unit>{
val asyncValue: Deferred<String> = async {
delay(5000)
"결과값"
}
// println(asyncValue) // 이렇게 하면 delay 때문에 기다리다가 종료되기 때문에 결과값을 받아올 수 없다.
println(asyncValue.await())
}
async는 비동기적으로 실행되는 코루틴을 만들고, 결과값을 반환할 수 있도록 한다.runBlocking이라 main 함수가 async 가 완료되기 전에 종료되진 않지만, 비동기적으로 실행하는 중에 println(asyncValue)를 만나면, 아직 결과값이 반환되기 이전이기 때문에 결과값을 받아올 수 없게 된다.asyncValue.await()을 사용할 수 있다. suspend fun fetchData() : String{
delay(5500)
return "받아온 데이터!"
}
fun main() = runBlocking<Unit>{
// println(fetchData()) // 얘를 이렇게 실행하면 5.5초 기다리기 때문
val result = async { fetchData() }
launch {
repeat(5) { i ->
delay(1000)
println("작업 처리 중... ${i+1}초 경과")
}
}
println("데이터 받아오는 중")
println(result.await())
}
suspend는 함수가 일시 중단(suspend)될 수 있다는 걸 나타내는 키워드이다.suspend 함수를 직접 호출하면, 해당 코루틴이 그 함수가 끝날 때까지 다음 줄로 못 넘어간다. 그렇기 때문에 println(fetchData()를 하면 5.5초를 기다렸다가 다음 코드가 실행된다.async를 통해 비동기적으로 실행하는 것이다.fun main() = runBlocking<Unit> {
val numbers = sequence {
println("yield(1)전")
yield(1)
println("yield(1)후")
println("yield(2)전")
yield(2)
println("yield(2)후")
println("yield(3)전")
yield(3)
println("yield(3)후")
}
launch {
for(num in numbers) {
println(num)
delay(5000)
}
}
}
sequence는 지연 시퀀스(lazy sequence)를 만드는 함수이다.yield를 사용해 값들을 순차적으로 생성한다.yield는 sequence 빌더 내부에서 값을 반환하는 키워드이다. yield(값)은 지금 바로 값을 반환하고 시퀀스 생성은 일시 중단한다.fun main() = runBlocking<Unit> {
launch {
repeat(3) {
println("작업 A 시작!")
yield()
println("작업 A 끝")
}
}
launch {
repeat(3) {
println("작업 B 시작!")
yield()
println("작업 B 끝")
}
}
}
yield()라는 suspend 함수이다.yield()는 sequence랑 다르게 코루틴 컨텍스트에서만 의미가 있다.count++를 동시에 하면 예상보다 증가가 덜 될 수 있다.var generalCount: Int = 0
var atomic = AtomicInteger(0)
suspend fun process1(action : suspend () -> Unit) {
var i = 100
var j = 100
val time = measureTimeMillis {
val services = List(i) {
GlobalScope.launch {
repeat(j) {
action()
}
}
}
services.forEach { it.join() }
}
println("${i * j}만큼의 작업을 수행하는데 ${time}ms 만큼의 시간이 소요되었습니다.")
}
fun main() = runBlocking<Unit> {
process1 {
generalCount++
}
println("generalCount=$generalCount") // 10000이 안 나온다.
process1 {
atomic.incrementAndGet()
}
println("atomic=$atomic") // 10000 나옴.
}
action 함수를 runBlocking 스코프에서 실행하고 있는 것이다. generalCount에 쓰기 작업을 하게 되면 원하는 결과값이 나오지 않게 된다. AtomicInteger을 사용하여 동시성 문제를 해결할 수 있다.val context = newSingleThreadContext("GreppContext")
var contextCount: Int = 0
suspend fun process2(action : suspend () -> Unit) {
var i = 100
var j = 100
val time = measureTimeMillis {
val services = List(i) {
GlobalScope.launch {
repeat(j) {
action()
}
}
}
services.forEach { it.join() }
}
println("${i * j}만큼의 작업을 수행하는데 ${time}ms 만큼의 시간이 소요되었습니다.")
}
fun main() = runBlocking<Unit> {
process2 {
println("쓰레드 - ${Thread.currentThread().name}")
withContext(context) {
println("쓰레드 - ${Thread.currentThread().name}")
contextCount++
}
}
println("generalCount=$contextCount")
}
newSingleThreadContext로 단일 스레드 컨텍스트를 생성한다. withContext(context)로 해당 로직은 하나의 스레드만 들어가서 실행할 수 있게 되는 것이다. contextCount 변수에 접근하지 못하게 되는 것이다. val mutex = Mutex()
var mutexCount = 0
suspend fun process3(action : suspend () -> Unit) {
var i = 100
var j = 100
val time = measureTimeMillis {
val services = List(i) {
GlobalScope.launch {
repeat(j) {
action()
}
}
}
services.forEach { it.join() }
}
println("${i * j}만큼의 작업을 수행하는데 ${time}ms 만큼의 시간이 소요되었습니다.")
}
fun main() = runBlocking<Unit> {
process3 {
mutex.withLock {
mutexCount++
}
}
println("mutexCount=$mutexCount")
}
withLock { ... } 안에 있는 코드 블록은 한 번에 하나의 코루틴만 실행할 수 있게 만든다고 볼 수 있다. 오늘은 RBF가 있는 날이었다. 나는 어제 강사님의 질문에.. 스터디로 공부한 건 기억나지만 희미하게 기억나고.. 이걸 말로 설명할 수 없음에 슬픔을 느끼고 주제를 '자바 프로그램 실행 과정 & JVM 구조'로 잡고 발표했다. 확실히 한 번 더 보니까 이해가 잘 되는구만 !!! CS는 정말 한 번 이해해도 금방 까먹어서 너무 슬픈 것 같다. 근데 이게 장기 지식으로 가기 위해서는 반복밖에 없는 것 같다. ㅠ
각자 다양한 주제를 가져와줬는데, 되게 유익했던 것 같다.
특히, Virtual Thread 주제가 되게 유익했다. 사실 운체 공부가 부족하다는 사실은 코루틴 때 너무 많이 느껴버려서.. 어렵다.. 이러고 있었는데 Virtual Thread를 주제로 설명해주셔서 스레드에 대한 이해와 멘토링 시간에 들었던 Virtual Thread가 더 이해가 잘 됐던 것 같다 !! 그리고 어려운 것 같은데 이걸 우째 이렇게 쉽게 설명해주시지.. 하는 대단함..
RBF 때문에 공부하신 게 아니라 멘토링 이후에 궁금해서 찾아보셨다고 하는 것도 대단했다. 다른 분들의 발표도 그렇고, 열심히 하시는 걸 보면 자극이 된다 ! 나도 열심히 해야지..
코루틴... 정말 열심히 따라가보려고 했으나 한 14...15 때부터 희미해졌다. 이해가 된 듯했지만, 그 다음 예시를 보니까 또 내가 이해한 게 맞나? 하는 너낌스.. 뭔가 virtual thread 설명도 듣고, 코드를 다시 보니까 더 이해가 잘된 것 같다. 그리고 운체가 부족하다는 것을 너무 크게 느꼈다. 멘토링 때도 느꼈는데 코루틴 수업 이후에는 더 느꼈다. ㅎㅅㅎ 스터디에서 운체 시작했으니까.. 똑띠 공부해야지 !!!!
코루틴 이해하니까 재밌긴 하네.. 하지만, 코드를 보고 좀 생각해야한다는 점 하하
벌써 코틀린 이론은 다 배웠다 ! 신기하네.. 일주일만에.. 허허