본문 바로가기
Apple/iOS

[iOS] 백그라운드 스레드에서 타이머 돌리기

by 어멘드 2022. 1. 15.
반응형

** 아직 공부하는 중이라 틀린 내용이 있을 수도 있습니다. **

 

 저번 글에서는 런루프와 타이머의 관계, 타이머 생성하는 법까지 다뤘다.

 

[Swift] Timer(타이머)와 Thread(스레드), RunLoop(런루프)

** 아직 공부하는 중이라 틀린 내용이 있을 수도 있습니다. **  최근 프로젝트에서 반복 타이머가 필요한 경우가 있었는데, 그때 알아보았던 타이머, 스레드, 런루프에 대해 까먹기 전에 정리하

please-amend.tistory.com

요약을 하자면..!

런루프는

  • 스레드마다 존재하면서
  • 마우스나 키보드 같은 입력 이벤트타이머 이벤트를 처리해준다.

타이머는

  • 객체를 만들어서 런루프에 추가해주어야 한다.
반응형

 그럼 타이머는 어느 스레드의 런루프에서 돌리는 것이 적절할까?

 메인 스레드에서는 UI와 터치 같은 사용자 입력을 처리한다. 따라서 메인 스레드가 바빠지면 타이머가 부정확해지거나 UI가 제대로 그려지지 않는 등의 문제가 생길 수 있다.

 실제로 프로젝트에서 1초마다 돌아가는 타이머를 메인 런루프에다가 default 모드로 추가해서 1초마다 UILabel을 업데이트하도록 했는데, 테이블 뷰를 스크롤하는 동안에는 타이머가 작동하지 않아 UILabel이 업데이트되지 않고 멈춰있었다.

 


 이 문제를 해결하는 방법이 있는데, default 모드가 아니라 common 모드로 추가해주는 것이다!

RunLoop.main.add(timer, forMode: .common)

 모드는 RunLoop Mode를 말하는데, 

 런루프는 입력 소스와 타이머 소스로부터 여러 이벤트를 받아 처리해준다고 했었다. 런루프 모드는 이 소스들 중에서 모니터링하고 싶은 소스들만 필터링을 할 수 있는 기능이다.

 런루프 모드의 종류는 common, default 외에도 3가지가 더 있다. 각각의 차이는 아직 정확히 이해를 못 했는데, 일단 서치 하면서 이해한 뉘앙스만 설명해보자면ㅠㅠ

 default 모드에서는 입력 소스의 이벤트를 처리하는 동안에는 타이머 소스는 모니터링을 하지 않는다. 그래서 가만히 있을 때는 UILabel이 잘 업데이트되다가도 화면 스크롤을 하는 동안에는 멈춰있었던 것이었다.

 common 모드에서는 타이머 소스도 같이 처리해주기 때문에 스크롤을 하는 도중에도 계속 UILabel은 잘 업데이트된다.

 


 저번에 그냥 넘어갔던 add(_: forMode:) 메소드에서 forMode가 바로 어느 런루프 모드를 정해주는 부분이다.

 RunLoop에 타이머를 생성해서 추가하는 방법은 2가지가 있다고 했었다.

 1. 생성과 동시에 현재 런루프에 자동 추가해주는 scheduledTimer

 2. init으로 생성 후 수동으로 add

 1번 자동 추가의 경우 default 모드로 추가가 된다. 그래서 위의 스크롤 문제를 해결하려면 모드를 직접 지정할 수 있는 init + add 방법을 써주어야 한다.

 


 일단 원하는 대로 굴러가긴 하는데, 아직 메인 스레드가 바쁘면 문제가 생길 수 있다는 점은 해결을 못했다. 방금 해결한 건 사용자 입력과 타이머가 동시에 처리되지 않는 문제만 해결한 것이다...

 해결 방법은 간단하다. 타이머를 메인이 아닌 백그라운드 스레드에 추가해주면 된다.

 근데 타이머 사용 예시 코드를 찾아보면 다들 메인에 추가해주던데, 이게 맞는 방법인지 의문이 들었다.

 

 런루프는 생성과 작동이 별개이다. 메인 런루프는 앱 실행과 동시에 자동으로 run 되는 반면, 메인이 아닌 백그라운드 스레드의 런루프는 생성은 자동으로 되지만, run은 개발자가 수동으로 시켜주어야 한다.

 run 메소드는 run(until:)이나 run(mode:before:)과 같이 만료 기한이 있는 run과 run()과 같이 영구적으로 도는 run이 있는데, 스레드 프로그래밍 가이드를 읽어보면 영구적으로 도는 run() 사용을 지양하고 만료 기한을 정해주도록 하라고 나와있다.

 공식문서의 run() 메소드 설명에도 런루프를 종료하려면 이 메소드를 쓰지 말고, 아래처럼 만료 기한이 있는 run을 반복적으로 돌려주고, 반복문 종료 조건을 컨트롤하는 방법을 쓰라고 나와있다.

var shouldKeepRunning = true	// 런루프를 종료하고 싶으면 false로 바꿔준다.

while shouldKeepRunning {
	RunLoop.current.run(until: .now + 0.1)
}

 


여기서 의문이 드는 게, 런루프 목적이 스레드가 일해야 할 때만 일하고 쉴 땐 쉬게 하는 거라고 했는데..

 저 while문을 돌리고 있는 스레드는 다른 걸 할 수 없다. 지금 이건 스레드 하나를 통째로 희생시켜서 런루프만 돌리고 있..?..???

 이것에 대해서는 아직까지 의문이 해결되지 않았다. 하지만 일단 공식문서에서 RunLoop를 종료하기 위한 방법으로 저 방법을 제시해줬고,

 스레드 프로그래밍 가이드의 "When Would You Use a Run Loop?" 챕터에 런루프를 시작해야 하는 4가지 경우 중 하나로 "스레드에서 타이머를 사용할 때"가 적혀있는 걸 봐서는,

 타이머를 백그라운드 스레드에서 돌려도 상관없고, 대신 해당 런루프를 run 시킬 때는 안전하게 run() 말고 만료 기한이 있는 run(until:) 같은 것을 쓰면 되는 것 같다!

 


 지금까지 알게 된 걸 바탕으로 백그라운드 스레드에 타이머를 추가하는 코드를 짜 봤다.

var shouldKeepRunning = true

DispatchQueue.global().async {
    print(RunLoop.current == RunLoop.main)

    Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
        print(Date())
    }

    while shouldKeepRunning {
        RunLoop.current.run(until: .now + 0.1)
    }
}

출력 결과

 DispatchQueue.global()를 써서 메인이 아닌 스레드로 가도록 했고, 확인을 위해서 처음에 메인 런루프인지를 출력해줬다.

 타이머는 1초마다 현재 시간을 출력하도록 했는데 출력 결과를 보면 잘 출력되고 있는 것을 확인할 수 있다!

반응형

댓글