1. Thread란 무엇이며, 왜 필요할까?
Thread란 컴퓨터 내부에서 하나의 흐름을 의미한다. 작업이 진행되는 하나하나의 흐름을 모두 Thread라고 부른다.
여기서 이러한 흐름이 하나인 경우 Single-Thread, 여러 가지인 경우 Multi-Thread라고 부른다.
Single-Thread는 하나의 흐름, 즉 하나의 동작밖에 하지 못하기 때문에 동시에 여러 작업을 하기 쉽지 않다.
우리가 사용하는 앱을 Single-Thread로 구현이야 할 수는 있지만 상당히 비 효율적일 것이다. 왜냐하면 하나의 작업만 가능하기에 다른 작업을 수행하기 위해서는 이전 작업이 완료되어야 가능하다.
예를 들어 카카오톡에서 X에게 동영상을 보내는 상황을 생각해 보자. 우리는 동영상이 다 전송되기 전까지는 다른 작업(동영상을 보내면서 X에게 다른 메시지를 전송한다거나, 다른 톡방에서 대화를 나누는 것과 같은 작업)을 할 수 없을 것이다. 왜냐하면 Single-Thread이기 때문에 동영상 전송이 완료되기 전까지는 다른 작업을 할 수 없기 때문이다.
반면 정해진 순서에 따라 한 번에 하나의 작업만 하는 경우에는 Single-Thread로 구현할 수 있다.
이와 같이 컴퓨터나 앱에서 여러 흐름, 작업을 동시에 하기 위해 대부분의 기능들은 Multi-Thread로 구현되어 있다.
2. Multi-Thread가 그럼 무조건 좋은 건가?
여러 작업을 동시에 수행할 수 있다는 점에서 Multi-Thread가 좋은 것은 사실이나, Multi-Thread에도 단점은 존재한다.
앞서 Multi-Thread가 여러 작업을 동시에 진행한다고 했지만 사실 엄밀히 따지면 같은 시간에 동시에 한꺼번에 작업하는 것은 아니다. 대신 여러 작업들 사이를 왔다 갔다 하면서 A작업 조금 하고, B작업 조금 하고... 이러한 방식으로 Context Switching을 하면서 여러 작업을 동시에 진행한다.
이런 점에서 보았을 때 Context Switching에서 시간이 소요되고, 또 빨리 끝내야 하는 작업이 있는 경우 다른 작업과 같이 진행하기 때문에 Single-Thread에 비해 이런 면에서는 비효율적이라고 볼 수 있다.
class MainActivity : AppCompatActivity() {
private lateinit var viewBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
Log.d("Lifecyle", "onCreate")
Thread(){ // 이 안에서 쓰는 것은 쓰레드로 작동하는 코드. 이 밖에서는 메인 쓰레드(UI 쓰레드)
for(i in 0 until 1001){
Log.d("Thread1","$i")
}
}.start()
Thread(){ // 이 안에서 쓰는 것은 쓰레드로 작동하는 코드. 이 밖에서는 메인 쓰레드(UI 쓰레드)
for(i in 1000 downTo 0){
Log.d("Thread2","$i")
}
}.start()
}
}
위 코드는 두 개의 Thread에서 각각 0부터 1000까지, 그리고 1000부터 0까지 로그를 찍어보는 코드이다. 이것을 실행해 보면 다음과 같다.
위와 같이 Thread1과 Thread2가 불규칙적으로 찔끔찔끔 왔다 갔다 하면서 작업이 진행되고 있는 것을 확인해 볼 수 있다.
3. Multi-Thread의 특징
Multi-Thread에서 각각의 흐름들은 자율성과 독립성을 가지고 있다. 즉, 각각의 흐름은 자율적으로 실행되고, 독립적으로 작업한다는 의미이다.
하지만 각각의 흐름들이 자율성과 독립성을 가지게 된다면, 전체적으로 제어하기 어렵다는 단점이 있다. 예를 들어 잔액이 만원인 계좌에서 Thread A와 Thread B가 동시에 만원을 출금하려고 한다면 각각의 흐름은 독립적이기 때문에 Thread A와 B는 서로가 공통된 계좌에서 출금한다는 사실을 모른다. 그렇게 되면 잔액이 만원인 계좌에서는 2만원이 출금되면서 돈 복사가 이루어진다.
따라서 하나의 Thread가 여러 Thread가 공유하고 있는 자원을 사용하려고 할 때에는 해당 자원을 사용한다고 Lock을 걸어서 다른 Thread가 접근하지 못하도록 한다.
추가) 동기 & 비동기 작업
1. 동기
우리가 그냥 평소에 작성하는 코드들은 모두 동기작업이라고 할 수 있다. 동기작업은 작업이 들어온 순서대로 결과가 도출된다. 작업의 순서대로 결과가 도출되기 때문에 작업 요청을 하면 그 즉시 작업을 수행해서 결과를 내야 한다.
장점: 원하는 일을 순차적으로 진행할 수 있다.
예를 들어 시간의 순서에 따라 진행해야 하는 코드인 경우가 있다. 이는 코드를 짜기에도 편하고 관리하기도 쉽다.
단점: Single-Thread와 비슷한 느낌으로 하던 일의 결과가 도출되기 전까지는 다른 작업을 진행하지 못한다.
2. 비동기
비동기 작업은 시간이 오래 걸리는 작업(100ms~500ms~1s 정도)에서 주로 사용한다. 대표적인 예로 통신할 때 적합하다.
비동기 작업은 동기 작업과는 다르게 작업이 들어온 순서와 상관없이 결과가 도출된다. 도출된 작업은 Callback 함수로 보낸다. 하지만 작업이 들어온 순서와 상관없이 진행되기 때문에 요청한 작업이 바로 실행된다는 보장은 없다.
장점: 여러 작업을 동시에 진행해야 할 때 유리하다.(Multi-Thread와 비슷한 느낌)
단점: 동기에 비해 작업들을 관리하기가 복잡하다.
비동기 작업에서 순차적으로 처리하려면 Callback 함수에서 비동기 작업을 실행하고 작업하고 Callback 하고... 이를 반복해야 한다.
예를 들어 서버에서 가져온 정보로 또 다른 정보를 가져오도록 활용해야 한다면 이전 작업이 완료되어야 다음 작업을 시작할 수 있으니까 순차적으로 정보들을 가져와야 한다. 이런 경우에는 Callback 함수 내에 또 다른 비동기 작업을 호출하는 코드를 작성해 주어야 한다.
이처럼 비동기 작업에서는 보다 복잡한 과정이 이루어지기 때문에 보다 효율적으로 비동기 프로그램을 구현하기 위해 코루틴(Coroutine)이라는 작업 패턴을 사용한다.