오늘은 Run loop에 대해서 공부해 볼 것이다.
이전 플젝에서 combine을 사용했었는데,
서버에서 가져온 데이터를 ui에 반영해 주기 위해 receive(on: Runloop.main) 메서드를 썼던 경험이 있다.
그때는 뭣도 모르고 ui 관련 동작이니 main 스레드에서 돌릴 때 쓰는 거구나 했는데 이번 기회에 자세히 알아볼 생각이다.
Run Loop?
우선 Run loop가 뭔지부터 알고 가자
apple document를 토대로 정리해 보면
- Run loop는 파일, 키보드, 마우스 등의 입력 소스를 처리하는 이벤트 처리 루프이다.
- Run loop의 관점에서는 특수한 케이스지만 Timer의 이벤트 또한 처리한다.
- 특정 이벤트가 왔을 때 Thread가 일해야 할 때는 일하고, 일이 없으면 쉬도록 하기 위해 애플에서 만든 스레드 관리 loop
Thread와 Run loop
스레드는 모두 각각의 Run loop를 가지며, 스레드가 생성될 때 Run loop가 자동으로 생성된다.
but, Run loop의 실행은 자동으로 되지 않고 직접 실행시켜 줘야 함 (단, Main Run loop만은 예외로 밑에서 다룰 예정)
그렇다면 Run loop는 언제, 어떻게 실행시킬까?
실행 방법에 대해 얘기하기 전에 작동 원리부터 알아보자
Run Loop의 작동 원리
Run loop는 루프를 수행할 때, 총 두 가지 Event source를 수신한다.
Input source
다른 스레드나 다른 응용 프로그램으로부터 온 비동기 이벤트를 전달한다.
Timer source
예정된 시간 또는 반복되는 간격으로 발생하는 동기 이벤트를 전달한다.
Thread에서 노란색 Run loop를 한 바퀴 도는 작업이 한 번의 실행이라고 생각했을 때
루프는 한 바퀴를 돌면서 Event들을 모아놨다가, 한꺼번에 처리하는 방식으로 작동한다.
이 Run loop는 한 번 event source를 읽고 전달하는 과정이 끝나면 그대로 대기한다.
즉, 내부적으로 반복 실행이 되지 않기 때문에
스레드 내에서 프로그래머가 while, for문을 통해 명시적으로 반복 실행시켜줘야 한다.
Run Loop 실행 방법
앞전에서
"스레드는 모두 각각의 Run loop를 가지며, 스레드가 생성될 때 Run loop가 자동으로 생성된다." 라고 했다.
스레드가 생성될 때 Run loop는 자동으로 생성되지만,
생성 후 실행은 프로그래머의 몫이 된다.
하지만 예외적으로 메인 스레드는 애플리케이션이 실행될 때 프레임워크 차원에서 Run loop를 자동으로 설정하고 실행한다.
이를 Main Run Loop라고 한다.
이제 런 루프를 직접 실행하는 방법에 대해 알아보자
런 루프를 실행하기 위해선 먼저 런 루프 객체에 접근해야 하는데, 이 경우 RunLoop의 프로퍼티인 current를 사용한다.
공식 문서를 보면
current의 반환 값은 현재 스레드에 대한 NSRunLoop 개체 라고 명시되어 있다.
또 스레드에 대한 런 루프가 아직 존재하지 않는다면 하나가 생성되어 반환된다고 한다.
다음은 현재 스레드에 대한 런 루프를 참조하는 방식이다.
let runLoop = RunLoop.current
런 루프를 실행시키기 위한 메서드로는 총 네 가지가 있다.
각각 간단하게 설명해 보자면
- run(): 실행할 당시, 루프에 들어와 있는 Input source와 Timer source를 영구적으로 처리한다.
DispatchQueue.global().async {
let runLoop = RunLoop.current
Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { _ in
print("Running run loop")
}
runLoop.run()
}
주의: 영구적으로 처리되기 때문에 실행 이후에 들어오는 이벤트는 이전 이벤트가 반환되기 전까지 처리되지 않으므로 유의한다.
DispatchQueue.global().async {
let runLoop = RunLoop.current
Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
print("Running timer1")
}
runLoop.run()
Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
print("Running timer2") // 실행 X
}
}
- run(until:): 가장 대표적으로 사용되는 메서드이다. 지정된 date까지 루프를 실행하고, 그 시간 동안 연결된 모든 input 데이터를 처리한다.
이전에 부착된 이벤트만 영구적으로 실행되는 run()과 달리,
이 메서드는 루프의 실행 시간을 지정해줄 수 있다.
실행 시간 동안 부착된 모든 이벤트를 처리해 주는 방식으로 작동한다.
보통 while, for문 등의 반복문을 써서 연속적으로 실행시킨다.
Run loop의 Mode와 관련된 실행 메서드
- run(mode:before:): 루프를 한 번 실행하고, 특정 모드에서 주어진 date까지 input을 blocking 한다.
- acceptInput(forMode:before:): 루프를 한 번 또는 주어진 date까지 실행하고, 특정 모드에 대한 input만 수락한다.
mode에 관한 내용은 Run loop 심화편에서 좀 더 자세히 다룰 예정이다.
언제 Run loop를 사용하는가
런 루프를 직접 사용하는 경우는 다음과 같은 작업을 할 때이다.
- Input source를 통해 다른 스레드와 통신하는 경우
- 타이머를 사용하는 경우
- performSelector를 사용하는 경우
- 주기적인 작업을 계속 수행해야 하는 경우
참고
https://developer.apple.com/documentation/foundation/runloop
https://babbab2.tistory.com/68
'Study > Swift' 카테고리의 다른 글
[Swift] 고차함수(1) - map, flatMap, compactMap (0) | 2022.09.18 |
---|---|
[Swift] RunLoop.Mode (0) | 2022.08.01 |
[Swift] Initializer 심화 (2) (0) | 2022.07.20 |
[Swift] Initializer 심화 (1) (0) | 2022.07.19 |
[Swift] Property (0) | 2022.07.10 |