서론
최근 Xcode의 Thread Sanitizer 기능에 대해 알게 되었습니다.
직접 사용해보니 흥미로워서 공유 목적으로 포스팅 작성합니다.
그리고 혹시 Sanitizer라는 영단어를 들어보셨나요?
전 이번에 처음 들어봤는데요.
소독제, 살균제라는 뜻이더라고요.
Thread를 소독해주는 기능(?)이라는 표현 같습니다 ㅋㅋ
Race Condition
TSan(Thread Sanitizer)는 런타임에 Race Condition을 탐지해 줍니다.
그래서 일단 Race Condition이 무엇인지 간단하게 살펴보도록 하겠습니다.
Race Condition이란 현재 작업 이외의 또다른 작업의 타이밍(작업순서)에 따라
결과가 달라져 여러 결과를 만들어낼 수 있는 상황을 말합니다.
생김새가 비슷한 단어로는 Data Race가 있는데요.
Data Race는 다른 곳에서 읽을 가능성이 있는 어떤 메모리 위치에 쓰기 작업을 하는 상황입니다.
일반적으로 Data Race는 Race Condition의 부분 집합입니다.
그렇지 않은 상황도 있지만, 이 포스팅에서 다루지는 않겠습니다.
지금 이 포스팅에서 알아야하는 것은 "Xcode의 TSan은 Race Condition을 감지해준다!"까지기 때문입니다.
Thread Sanitizer 사용 방법
Xcode에서 TSan을 사용하는 법을 알아봅시다. (Xcode 14.3.1 버전)
1.
Xcode 프로젝트에서 Edit Schema를 선택합니다.
위와 같은 창이 나오면 됩니다.
2.
Runtime Sanitization의 Thread Sanitizer를 선택합니다.
Thread Sanitizer를 선택하면 비활성화되는 항목이 있을겁니다.
그 항목들은 Thread Sanitizer와 동시에 사용하지 못하는 항목이니 크게 신경 쓰지 않아도 됩니다.
스크린샷에 Main Thread Checker도 보이시죠?
저희가 UI 작업을 할 때 Main Thread가 아닌 곳에서 진행하면 보라색 경고 표시가 나는데
그게 바로 Main Thread Checker의 기능입니다.
UI 작업은 반드시 Main Thread에서 이뤄져야 하므로 기본값이 On 입니다.
3.
빌드를 합니다.
빌드는 처음부터 진행이 됩니다.
왜 처음부터 진행이 되는지는 아래에서 간단하게 다룰 TSan의 원리를 보면 이해가 되실듯 하네요.
(근데 너무 간단하게 다뤄서 이해가 안 될 수도...)
결과
주의 : 컴파일 타임에 경고가 나오는 것이 아닙니다! 런타임에 경고가 나오니 시나리오대로 앱을 동작시켜야 합니다.
런타임에 Race Condition이 감지되면 코드와 좌측의 Issue Navigator에 경고 표시가 나옵니다.
동시에 커널에도 경고 문구가 나오는데요.
여러 Race Condition 중 어떤 상태인지, 왜 발생했는지, 어떤 스레드에서 발생했는지 자세히 적혀 있습니다.
저는 thread T2에서 Write를 하는데 Main Thread에서 Read를 해서 Data Race가 발생한다고 나와 있네요.
이후 내용에는 Thread T2가 running 중이고 GCD worker thread라는 걸 알려줍니다.
Thread Sanitizer 원리
Thread Sanitizer는 어떻게 Race Condition 지점을 알 수 있는걸까요?
심지어 코드의 위치까지 딱 알려주는데 어떻게 가능한걸까요?
바로 컴파일 타임에 메모리 체크 코드를 삽입하기 때문입니다.
먼저 Swift 컴파일 과정에 대해 가볍게 알아봅시다.
Swift 컴파일러는 우리의 Swift 코드를 파싱하고 Abstract Syntax Tree(AST)로 변환합니다.
Semantic Analysis 단계에서 AST를 이용해 타입 체크된 AST(type-checked AST)를 만들며, Semantic issues를 확인합니다.
이후 SILGen(Swift Intermediate Language Generation) 단계에서 type-checked AST를 이용해 raw SIL로 변환합니다.
그리고 Generic specialisation과 ARC 최적화같은 최적화 작업을 거치게 됩니다.
이 최적화된 SIL을 IRGen에 전달하여 IR(Intermediate Representation)을 생성하고,
LLVM은 전달받은 IR을 이용해 object 파일을 생성합니다.
Thread Sanitizer은 LLVM IR level에서 메모리 체크 코드를 삽입합니다.
그래서 컴파일을 다시 해야하는거죠!
IR -> LLVM으로 진행될 때 코드를 삽입하기 때문에
어디서 Race Condition이 발생했는지 정확히 짚어줄 수 있었던 거에요.
Thread Sanitizer 주의점
Thread Sanitizer를 사용할 때 주의점이 있습니다.
첫 번째로 실제 기기에서는 이 기능이 동작하지 않습니다.
오직 64비트 macOS, iOS , tvOS, watchOS 시뮬레이터에서만 동작합니다.
그러므로 실기기 디버깅용으로는 사용하지 못합니다.
또한 성능 오버헤드가 존재한다는 것입니다.
관련 공식문서(참고 1번 링크)에서는 2배에서 20배까지 속도 저하가 발생할 수 있고,
메모리 사용량은 5~10배까지 증가할 수 있다고 합니다.
따라서 매번 Thread Sanitizer를 키는게 아니라 Race Condition 테스트를 할 때만 키는 것을 추천합니다.
마지막으로 Thread Sanitizer를 맹신하면 안 된다고 합니다.
여러 글을 찾아보니 100% 정확도를 가지지는 않는다고 하네요.
Thread Sanitizer에 의존하기 보다는 Race Condition을 이해하고 예방하는 게 베스트겠습니다.
access race와 data race
마무리를 하기 전에 궁금증 하나만 해결하고 가겠습니다.
access race와 data race가 무슨 차이가 있는지 궁금했습니다.
data race는 너무 익숙한 단어인데 access race는 처음 본 문제였거든요.
스레드 간의 경쟁 조건에서 두 개 이상의 스레드가 하나의 공유 자원에
- accesss race : 접근하는 모든 상황을 일반적으로 포괄하는 용어
- data race : 접근하여 최소한 하나 이상의 스레드가 쓰기 작업을 수행하는 상황
이라고 합니다.
즉, 쓰기 작업의 유무가 가장 큰 차이점이군요!
마무리
Xcode의 Thread Sanitizer 기능에 대해 알아봤습니다.
분명 편리한 기능이지만, Race Condition에 대한 근본적인 이해가 중요해 보입니다.
감사합니다.
참고
https://developer.apple.com/documentation/xcode/diagnosing-memory-thread-and-crash-issues-early
https://developer.apple.com/documentation/xcode/data-races
https://medium.com/@lucianoalmeida1/i-little-bit-about-thread-sanitizer-56a887dc144
https://velog.io/@xiniha/Race-Condition과-Data-Race-알아보기
https://medium.com/@nitingeorge_39047/xcode-thread-sanitizer-f91435fcb1db
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.