본문 바로가기

iOS/PROJECT

UI업데이트를 메인스레드에서 해야하는 이유 with 데드락

문제 ㅇㅅㅇ

카테고리를 선택해서 필터링 된 리스트를 보여주는 화면에서 문제가 발생했다.

 

1. 앱이 튕기거나

2. 화면이 멈춰서 아무런 동작을 할 수 없었다. (앱은 돌아가지만 화면 터치가 막힌 상태)

 

 

자세히 살펴보면 데드락이 발생했고,

outstanding async updates (비동기적으로 업데이트가 발생한 상태에서 해당 업데이트가 완료되기 전에 다른 업데이트가 추가로 발생하는 것) 가 있는 메인큐에서 이 메서드를 호출하는 것은 허용되지 않는다고 한다.

 

문제가 됐던 코드

원인이 되는 코드를 찾아 스레드를 거슬러 올라가보니 컬렉션 뷰의 데이터를 업데이트하는 코드를 찾을 수 있었다.

 

이 문제가 되는 코드를 빠르게 찾을 수 없었던 이유가..

어째서인지 문제 발생한 코드는 단순히 subscribe를 하고 있었고,

전체 목록을 가져와 컬렉션뷰에 뿌려주는 코드는 drive 였기 때문이었다. (drive는 메인스레드 동작이라 잘 굴러갔던 것..)

(왜 이렇게 코드를 짰을까,,?)

 

더 상세한 로직 문제가 있지만 결국 문제의 원인은 UI업데이트를 백그라운드 스레드에서 했기 때문 이었다.

 

 

사실 저 생각이 바로 안나서 두시간동안 스레드랑 콘솔창만 째려보고 있었다..ㅋㅋ

 

바로 백그라운드 스레드에서 UI를 업데이트해서 발생한 오류라고 생각되지 않았던 이유가

보통 여태 내가 보았던 UI 업데이트 오류는 아래 사진처럼 보라색으로..

메인 스레드에서만 업데이트 되어야 한다고 명시적으로 알려줬기 때문이었다.

 

메인 스레드에서만 UI를 업데이트 해야한다! 는 알았는데

백그라운드 스레드에서 UI를 업데이트하면 데드락이 발생할 수 있다! 를 생각지 못한 것이었다..ㅇㅅㅇ

 

 

'왜 메인스레드에서 UI 업데이트를 해야할까' 를 좀 더 알아보자 + 애플이 어떻게 방지하고 있는지

 

여기서 발생한 의문은

 

1. 보통은 보라색으로 엑스코드가 백그라운드 스레드의 UI 업데이트를 감지하는데 어떻게 감지하는건지

2. 왜 내 프로젝트에선 감지를 못하고 데드락이라고 표시가 나서 튕기거나 멈추는건지,,

 

 

 

 

첫번째로 백그라운드 스레드의 UI 업데이트를 감지하는데 어떻게 감지했는지는 콘솔을 보고 바로 확인할 수 있었다.

 

Main Thread Checker 라는 요놈이 UI API가 백그라운드 스레드에서 실행됐다는 것을 감지한 것이였다.

 

  • Main Thread Checker—This tool verifies that system APIs that must run on the main thread actually do run on that thread.

공식 문서를 보면  Main Thread Checker는 메인 스레드에서 실행되어야 하는 시스템 API가 실제로 메인스레드에서 실행되는지 확인하는 도구라고 한다.

 

대략 UIKit, AppKit 일부 시스템 API를 확인한다고 생각하면 될 것 같다.

 

동작 방식은 시스템 메서드를 동적으로 현재 스레드를 확인하는 메서드로 변형시켜서 스레드를 확인하는 것 같다. (번역이.. ㅇㅅㅇ)

The Main Thread Checker tool dynamically replaces system methods that must execute on the main thread with variants that check the current thread. The tool replaces only system APIs with well-known thread requirements, and doesn’t replace all system APIs. Because the replacements occur in system frameworks, Main Thread Checker doesn’t require you to recompile your app.

Because Main Thread Checker doesn’t require you to recompile your code, you can run it on an existing macOS binary. Inject the dynamic library located at 
/Applications/Xcode.app/Contents/Developer/usr/lib/libMainThreadChecker.dylib
 into your executable.

아무튼 현재 스레드를 확인하는 메서드로 교체되는 것이 시스템 프레임워크에서 일어나기 때문에

Main Thread Checker 요놈 때문에 다시 앱을 컴파일 할 필요가 없다고 한다.

때문에 다이나믹 라이브러리를 넣어서 macOS 바이너리를 실행할 수 있어서 성능에 영향을 별로 안미친다.

 

- 프로세스 1~2%의 CPU 오버헤드,

- 프로세스 시작 시간을 100밀리초 이하로 늘림

 

그래서 기본적으로 엑스코드를 실행하면  Main Thread Checker가 체크되어 있다.

요놈이 보라색 경고를 날려주는 것이다.

 

이 체크 표시는 edit scheme 에서 볼 수 있다.

 

그렇다면 위에 보라색으로 경고를 날렸던 코드에서 Checker 를 해제하면 ?

 

 

보라색 경고가 발생하지 않고 실행할 때마다 UI 업데이트 결과가 달라지는 모습을 볼 수 있었다.

 

 

 

UI 업데이트를 백그라운드 스레드에서 업데이트하면 데드락이 발생한다?

 

그렇다면 두번째 의문이었던

 

2. 왜 내 프로젝트에선 감지를 못하고 데드락이라고 표시가 나서 튕기거나 멈추는건지,,

 

내가 내린 결론은 (정확하지 않을 수 있다..ㅠ)

Main Thread Checker가 동작하기 전에

데드락이 걸려 앱이 진행되지 못했기 때문에 보라색 오류를 보지 못했던 것 같다.

 

그림에 선 추가하는 기능이 없넹..

 

그렇다면 내 코드에서 UI 업데이트가 어떻게 데드락을 발생시켰을까 ???

 

코드를 다시 살펴보고 데드락의 원인을 생각해보면서 원인을 알 수 있었다.

 

메인 스레드와 백그라운드 스레드에서 컬렉션 뷰를 동시에 업데이트 하고 있는 중이었다.

 

이 중에 하나를 주석처리하면 데드락 발생 노노,,

 

멈춘 뷰의 스레드 상태를 보면

Thread 8에서 데이터 소스의 apply를 적용하고 lock wait 상태인 것을 볼 수 있다.

메인 스레드에서도 데이터 소스의 apply 후 멈춰있는 모습을 볼 수 있다.

 

또 내가 혼자 내린 결론은 .. ㅋㅋ (정확하지 않다..ㅠ)

 

백그라운드 스레드에서 컬렉션 뷰 UI를 업데이트 하기 위해

이를 처리하기 위한 메인 스레드를 호출했고 대기 상태로 바뀜 (wait) (이 부분이 정확한지 모르겠다. wait 걸길래 내부적으로 메인에서 UI를 업데이트하게 동작하도록 될 것 같다는 생각..)

 

동시에 메인 스레드에서도 컬렉션 뷰 업데이트를 해야하는데

 

백그라운드 스레드 - 메인 스레드에서 뷰 업데이트 하기를 기다림

메인 스레드 - 백그라운드 스레드가 요청한 뷰 업데이트 하기를 기다림

 

이렇게 서로 기다리는 상태가 되어서 데드락이 발생한 것 같다.

 

 

해결 방법

1. 둘다 메인 스레드에서 업데이트 하거나

2. 두 개의 코드를 합치는 것 (전체 매치 불러옴 + 현재 카테고리로 분류)

 

결론

UI 업데이트는 메인 스레드에서만 동작해야 한다.

 

왜냐하면 UIKit 은 thread-safe 하지 않고, 만약 UI 업데이트를 백그라운드 스레드에서 하려고 하면 race-condition, deadlock 같은 동시성 문제가 발생할 수 있다.

 

race-condition 문제가 발생하는 경우는 같은 UI를 메인과 백그라운드 스레드에서 동시에 업데이트 하려고 했을 때 발생한다.

원하는 결과가 나오지 않을 수도 있다.

 

deadlock은 위의 문제에서 백그라운드와 메인스레드에서 동시에 업데이트 하면 발생한다.

 

애플에서는 이를 방지 하기 위해 Xcode에서 아래 링크에 있는 다양한 도구들을 지원한다.

(백그라운드 UI 업데이트 감지, 데드락 감지 등)

 

결론은 코드 잘짜고 테스트 잘하자

 

 

https://developer.apple.com/documentation/xcode/diagnosing-memory-thread-and-crash-issues-early

 

Diagnosing memory, thread, and crash issues early | Apple Developer Documentation

Identify runtime crashes and undefined behaviors in your app during testing using Xcode’s sanitizer tools.

developer.apple.com