본문 바로가기
iOS/이슈

[iOS Issues] NWPathMonitor thread safety crash issue

by Sky Titan 2024. 11. 10.
728x90

https://developer.apple.com/documentation/network/nwpathmonitor

 

NWPathMonitor | Apple Developer Documentation

An observer that you use to monitor and react to network changes.

developer.apple.com

네트워크 상태 탐지를 위한 API인 NWPathMonitor 사용 중 발생한 thread safety issue이다.

 

근본적인 원인은 이전에 공유한 thread safety issue와 같다.

 

[iOS Issue] Multi thread 환경에서 Dangling Pointer Crash 이슈

앱의 Logging을 담당하는 모듈에서 multi thread 환경에서 동작 시 잦은 Crash가 발생하는 이슈가 생겼다. 결론만 말하자면 원인은 Singletone 클래스에 있는 Thread-safety 처리가 되어있지 않은 stored property

skytitan.tistory.com

 

 

NWPathMonitor에서는 네트워크 상태 변화를 observing 할 때 사용한 DispatchQueue를 지정할 수 있게 되어있다.

아마 지속적으로 observing을 하는 행위 자체가 꽤 cost를 요구하는 일이기 때문에 main queue에서 진행하지 말기를 권장하는 차원에서 만들어 놓은 interface인 듯 하다.

start함수에서 parameter로 queue를 넘겨줄 수 있다.

 

Root cause


아래는 문제가 되었던 코드이다.

import UIKit
import Network

class NetworkObserver {
    private let monitoringQueue = DispatchQueue(label: "com.monitoring.queue")
    private let monitor = NWPathMonitor()
    static let shared = NetworkObserver()
    
    var isWifi: Bool {
        return monitor.currentPath.usesInterfaceType(.wifi) // Main thread에서 접근
    }
    private init() {
        monitor.start(queue: monitoringQueue) // Worker thread에서 업데이트
    }
}

 

 해당 코드 실행 시 monitor.currentPath는 Network 환경이 바뀔 때마다 monitoringQueue에서 업데이트가 된다.

하지만 currentPath에 access하는 isWifi block을 main thread나 다른 스레드에서 접근이 가능하도록 되어있어, currentPath의 setter와 getter가 동시에 다른 스레드에서 호출 시 dagling pointer 크래시가 발생할 수 있다.

 

 

Solution


Short-term

아래는 수정된 코드이다.

import UIKit
import Network

class NetworkObserver {
    private let monitoringQueue = DispatchQueue(label: "com.monitoring.queue")
    private let monitor = NWPathMonitor()
    
    private(set) var isWifi: Bool = false
    static let shared = NetworkObserver()
    private init() {
        monitor.pathUpdateHandler = { [weak self] path in
            guard let self = self else { return }
            self.isWifi = path.usesInterfaceType(.wifi)
        }
        monitor.start(queue: monitoringQueue)
    }
}

 

NWPathMonitor는 pathUpdateHandler라고 해서, NWPath가 업데이트 될 때마다 호출되는 block을 지정을 해줄 수 있다.

(일반적으로 이런 경우엔 delegate가 제공되는데 이 API 제작자는 무슨 의도인진 모르겠지만 block하나만 지정할 수 있게 해주었다.)

 

isWifi라고 하는 boolean 프로퍼티를 하나두고 block에서 NWPath가 업데이트 될 때마다 업데이트하도록 하는 것이다. Bool type의 경우엔 struct이기 때문에 태생적으로 thread safety해서 위에서 언급한 dangling pointer이슈가 발생하지 않는다.

 

 

Mid-term

 여기서 좀 더 나아가 좀 더 근본적이고 똑똑한 해결법을 생각해보았을 땐 아래와 같이 우리쪽 코드에 currentPath 프로퍼티를 두고 getter와 setter와 thread safety 처리를 한 뒤 강한 참조로 들고 있는 방법도 있다.

import UIKit
import Network

class NetworkObserver {
    private let monitoringQueue = DispatchQueue(label: "com.monitoring.queue", attributes: .concurrent)
    private let monitor = NWPathMonitor()
    
    var isWifi: Bool {
        currentPath.usesInterfaceType(.wifi)
    }
    
    private(set) var currentPath: NWPath {
        get {
            monitoringQueue.sync {
                return _currentPath
            }
        }
        
        set {
            monitoringQueue.async(flags: .barrier) {
                self._currentPath = newValue
            }
        }
    }
    private lazy var _currentPath: NWPath = monitor.currentPath
    
    static let shared = NetworkObserver()
    
    private init() {
        monitor.pathUpdateHandler = { [weak self] path in
            guard let self = self else { return }
            self.currentPath = path
        }
        monitor.start(queue: monitoringQueue)
    }
}

 

728x90

댓글