Swift Thread RunLoop

Swift Thread RunLoop

사진 설명이 없습니다.

스레드는 자유롭게 Main Thread외에 다른 일을 해야할 떄 생성하고 일 주기가 편하다.

하지만 아무렇게나 자유롭게는 사용하기 힘듬

왜냐하면 잘 못쓰면 스레드가 너무 많아서 앱이 버거워하기 때문이다.

편안하지만 편안하게 못쓰는 스레드 그 스레드의 생명주기 RunLoop에 대해서 알아보자


RunLoop

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

  • RunLoop는 스레드 당 하나씩 할당된다. a.k.a 1대 1 마크

  • 스레드에 작업이 생기면 처리하고, 아닐 때에는 대기시키는 역할

  • RunLoop는 메인 스레드를 제외한 스레드는 자동으로 만들어지지 않음

    • 개발자가 달라고 해야 그제서야 만들어서 줌

이걸 왜 써야 할까?

DispatchQueue.global().async {
    Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in
        print("⏰⏰⏰⏰⏰⏰⏰")
    }
}

sleep(10)
print("Main Thread Off")
/// Main Thread Off

위 코드 실행하면 ⏰가 출력이 안된다. 왜 일까?

한 번 알아보자 아이브의 러브 다이브 대신 숨 참고 RunLoop 다이브

스레드는 저 노란색 Track 처럼 한 바퀴 돌면서 생명주기를 한다.

만약 처리할게 없다면? Start -> End로 한 번 질주하고 스레드는 쉰다.

엥? 그러면 안되는데 Timer sources 받을 때 까지 뛰어야 하는데??

위 이미지 처럼 RunLoop가 순환하며 Timer Sources를 수신하고 출력해야하지만

Start -> End까지만 달리고 나가버린 것이다!

안돼! 어떻게 RunLoop를 저 트랙(노란색) 위에 돌게 만들지??

RunLoop 공식 문서를 보자

RunLoop 애플 공식 홈페이지 Overview를 보면

RunLoop 객체는 마우스 및 키보드 이벤트와 같은 소스에 대한 입력을 처리합니다. RunLoop 객체는 타이머 이벤트도 처리합니다.

애플리케이션에서 RunLoop 객체를 생성하거나 명시적으로 관리하지 않습니다. 시스템은 애플리케이션의 메인 스레드를 포함하여 각 스레드 객체에 대해 필요에 따라 RunLoop 객체를 만듭니다. 현재 스레드의 실행 루프에 액세스해야 하는 경우, current 메소드를 사용하세요.

RunLoop의 관점에서 볼 때, 타이머 객체는 "입력"이 아니며, 특별한 유형이며, 타이머가 끝날 때 실행 루프가 돌아오지 않도록 합니다.

  • RunLoop는 필요에 따라 만든다. -> 부르기 전까지 안 만든다. currnet 메소드로 호출해라.

  • 시스템에서 관리 안하니깐 너가 만들면 너가 관리해야한다? 못하면 너.책.임

오 그럼 RunLoop를 통해 스레드 관리를 어떻게 해야할까?

공식 개발 문서에 Loop를 뛰게 하는 방법에 대해서 쓰여져 있다.

여기서는 run()run(until: Date)에 대해서 알아보자.

run()

  • 말 그대로 기약없이 계속 Loop를 돌리게 만드는 방법이다.

run(until: Date)

  • Date 약속된 시간까지 Loop를 돌리게 하는 방법이다.

왜 여기서 제한을 두는 것일까? 스레드가 더 이상 할 일이 없으면 그만 돌라고 자동화 시켜 놓으면 안되나?

-> 응 안된다. 왜냐하면 시스템에서 관리 안한다. 즉 개발자가 알아서 관리해야한다.

그럼 ⏰가 출력 안되었던 것을 고쳐보자.

  1. RunLoop를 호출해서 만들어야 할 것이다.

    • let runLoop = RunLoop.current
  2. RunLoop를 통해 스레드가 일정시간 트랙을 달릴 수 있도록 할 것이다.

    • runLoop를 실행시켜야 할 것이다. run()함수를 써보자
DispatchQueue.global().async {
    let runloop = RunLoop.current
    Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
        print("⏰⏰⏰⏰⏰⏰⏰")
    }
    runloop.run()
}

sleep(17)
print("Main Thread Off")
/*
⏰⏰⏰⏰⏰⏰⏰
⏰⏰⏰⏰⏰⏰⏰
⏰⏰⏰⏰⏰⏰⏰
⏰⏰⏰⏰⏰⏰⏰
⏰⏰⏰⏰⏰⏰⏰
Main Thread Off
*/

돌려보면 ⏰가 총 5개 줄 출력되고 Main Thread Off가 출력되면서 프로그램이 종료될 것이다.

하지만 이 코드는 좋지 않다.

왜냐고? 지금은 Main Thread가 종료되면서 DispatchQueue가 종료된 것이지 다른 작업을 하면 RunLoop는 계속 실행되고 있을 것이다.

즉, RunLoop를 제약없이 실행되게 만들었다.

결국 나중에는 RunLoop가 종료되지 않았기에 스레드가 계속 작동하여 자원낭비를 초래할 수 있다.

DispatchQueue.global().async {
    let runloop = RunLoop.current
    Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
        print("⏰⏰⏰⏰⏰⏰⏰")
    }
    runloop.run(until: .now + 10)
}

sleep(17)
print("Main Thread Off")
/*
⏰⏰⏰⏰⏰⏰⏰
⏰⏰⏰⏰⏰⏰⏰
⏰⏰⏰⏰⏰⏰⏰
Main Thread Off
*/

위 코드는 ⏰가 총 3개 줄이 출력 될 것이다.

RunLoop가 지금으로부터 10초간만 동작하기에 10 / 3 = 3 번 출력만 할 것이다.

주의해야 할 점!!

import Foundation

DispatchQueue.global().async {
    let runloop = RunLoop.current
    runloop.run(until: .now + 10)  // <- 여기 순서가 다름
    Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
        print("⏰⏰⏰⏰⏰⏰⏰")
    }
}

sleep(17)
print("Main Thread Off")

RunLoop의 run(until: .now + 10)를 일찍 호출 시기키면 타이머가 정상적으로 출력하지 않는다.

엥? 이건 왜 출력이 안되는거지?

이유는 간단하다. Timer sources 가 실행되기 전에 RunLoop를 작동시켰기 때문이다.

RunLoop를 작동시키기 전에 입력 받을 소스를 미리 스레드에 입력시켜야 함을 잊지 말자


참고한 사이트