iOS 공부하는 감자

iOS) Run Loop 본문

iOS

iOS) Run Loop

DongTaTo 2022. 8. 8. 09:04
반응형

RunLoop란?

런루프 객체는 소켓, 파일, 키보드, 마우스 등의 입력 소스와 타이머 이벤트 등을 수신하고 처리하는 이벤트 처리 루프이다.

특정 이벤트가 발생했을 때, 스레드가 일해야 할 때는 일하고, 일이 없으면 쉬도록 하기 위해 애플에서 만든 스레드 관리 Loop이다.

RunLoop가 수신하는 이벤트

  1. Input Sources : 다른 스레드나 다른 응용프로그램의 비동기 이벤트를 수신하고, 이벤트 핸들러를 수행한다.
  2. Timer Sources : 예정된 시간이나 반복되는 간격으로 발생하는 동기 이벤트를 수신하고, 이벤트 핸들러를 수행한다.

 

 

각 스레드는 1개의 RunLoop를 가지며, RunLoop는 수신받은 이벤트를 모아 두었다가 실행되는 순간에 모아두었던 이벤트에 대한 핸들링 처리를 수행한다.

(+ 특정 실시간 모니터링 모드가 아니라면, 수신된 이벤트는 RunLoop가 실행되기 전까지 모아져서 대기하고 있다가 RunLoop가 실행되면 각 수신 이벤트들에 대한 핸들러 처리를 수행)

 

 

 

RunLoop의 생성과 실행

비동기 처리를 위해 스레드 객체를 사용할 때, 자동으로 RunLoop가 생성되지만, 메인 스레드를 제외한 스레드에서는 RunLoop가 자동으로 실행되지는 않는다.

 

즉, DispatchQueue를 통해 스레드 객체를 생성하면 RunLoop도 같이 생성되지만, 자동으로 실행되지는 않아서 개발자가 직접 실행시켜야 한다.

 

그리고 RunLoop는 무한 반복적으로 실행되는 것이 아니라, 한 번의 루프가 도는 동안 수신받은 이벤트에 대한 핸들러를 수행한 후 대기상태로 돌아간다. 따라서 반복 실행이 필요한 경우 개발자가 반복문을 사용하여 RunLoop가 계속 실행되도록 구현해야 한다.

(메인 스레드의 RunLoop는 자동으로 반복 실행됨 > 따로 실행시켜줄 필요 없다.)

 

 

 

 

스레드에서 RunLoop를 사용하는 방법

우선 다음과 같은 방법으로 현재 동작하는 스레드에 대한 RunLoop를 받을 수 있다.

let runLoop = RunLoop.current

 

 

이벤트 수신을 위해 RunLoop를 직접 실행시켜야 하는데, 4가지 방법(메서드)을 통해 실행시킬 수 있다.

2, 4번째 메서드는 mode에 대한 추가정보가 필요해서 패스..

 

1. run()

수신받은 이벤트를 영구 Loop에 넣고, 이 시간 동안 연결된 모든 Input Sourced의 데이터를 처리한다.

DispatchQueue.global().async {
    
    // 런루프 객체
    let runLoop = RunLoop.current

    // 타이머 이벤트 수신
    Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
        print("HI")
    }

    // 런루프 실행
    runLoop.run()

    // 타이머 이벤트 수신
    Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
        print("HELLO")
    }
}

// print("HI")만 3초마다 무한 반복 실행

위 코드에서 런루프가 실행되는 시점에 부착되어 있는 타이머 이벤트만 영구 Loop로 들어가서 영구적으로 반복 실행된다.

만약 run() 메서드를 실행시키는 시점에 부착된 이벤트가 없다면, 즉시 종료된다.

 

 

3. run(until: Date)

가장 많이 사용되는 방식으로, 매개변수로 입력되는 날짜까지 루프를 실행시킨다.

해당 기간동안 RunLoop는 부착되어 있는 모든 이벤트에 대한 핸들링 처리를 수행한다.

 

보통 아래와 같은 형태로 반복문(while)과 함께 사용한다.

addingTimeInterval()메서드로 현재 시점을 기준, 몇 초 동안 RunLoop를 실행시킬지 결정하며, 보통 0.1 ~ 1을 사용한다.

(while문으로 계속 반복 실행해서, run()메서드가 중복되는 것 아닌가? 생각했지만, 테스트해보니 RunLoop가 종료되어야 반복문이 다시 실행됨 => 3초로 설정하면 while문은 3초마다 실행)

// RunLoop의 종료가 필요한 시점에 false로 변경하여 RunLoop를 종료시킴
var isRun: Bool = true


while isRun {
    runLoop.run(until: Date().addingTimeInterval(1))
}

 

 


 

Timer Sources의 동작 시점

Timer는 설정한 시간을 기반으로 특정 동작을 수행하지만, 실시간 메커니즘으로 동작하지 않는다.

Input Sources와 마찬가지로 Timer가 현재 RunLoop에 의해 실시간 모니터링 모드에 있지 않다면, RunLoop가 실행되기 전까지 타이머는 실행되지 않는다.

그리고, RunLoop가 핸들러 루틴을 실행하는 도중에 타이머가 실행되면, 해당 타이머는 다음 RunLoop의 핸들러 루틴을 기다리며, 다시 RunLoop가 실행되지 않으면 타이머 역시 실행되지 않는다.

DispatchQueue.global().async {
    
    // 런루프 객체
    let runLoop = RunLoop.current

    // 타이머 이벤트 수신
    Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
        print("HI")
    }

    // 런루프 실행
    runLoop.run(until: Date().addingTimeInterval(6))
    
    // 타이머 이벤트 수신
    Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
        print("HELLO")
    }
}

// HI만 2번 출력됨

 

반복 타이머의 경우, 실제 실행 시간이 아닌 예정된 실행 시간을 기준으로 일정을 자동 조절한다.

타이머가 특정 시간을 기준으로 이후 5초마다 반복 실행되도록 예약된 경우, 실제 실행 시간이 지연되더라도 설정했던 시간을 기준으로 5초 간격으로 실행된다. 

(현재 시간으로부터 10초 이후부터 실행되어 반복되는 타이머가 있다면, 5초가 지난 후 타이머가 실행되어도 원래의 예정된 루틴대로 타이머가 실행된다는 의미인듯)

 

만약 실행 시간이 너무 지연되어 예정된 실행을 하나 이상 놓친 경우, 타이머는 놓친 작업에 대해 딱 한 번만 실행되며, 놓친 작업에 대한 실행이 완료된 후 타이머는 예정된 실행 시간으로 다시 예약된다.

(작업을 놓치면 타이머가 실행되는 시점에 놓친 작업에 대한 처리를 한 후, 놓치지 않았을 경우 실행되었을 루틴대로 실행된다는 의미인듯)

 

 


 

[주의사항]

RunLoop에 대한 공식문서를 보면 새빨간 경고 문구를 확인할 수 있다.

RunLoop는 thread-safe하게 설계되지 않았기 때문에, RunLoop의 인스턴스 메서드를 사용하려면 반드시 현재 스레드에서 실행해야 하며, 다른 스레드의 RunLoop 메서드는 건들지 말라고 한다. 

 

반응형