openlang

반복 Timer의 unowned self, 크래시를 미룰 뿐

반복 Timer 블록에서 옵셔널을 없애려고 [weak self]를 [unowned self]로 바꾸고 화면을 닫았어요. 다음 틱에서 크래시가 났어요.

이상할 건 없어요. 런 루프가 예약된 타이머를 살려 두고, 타이머는 자기 블록을 쥐고 있어요. 그래서 누가 invalidate()를 부르기 전까지 클로저는 컨트롤러보다 오래 살아요. unowned는 컨트롤러가 클로저보다 오래 산다고 Swift에 말하는 건데 여기서는 정반대고, 해제된 뒤 unowned 참조에 접근하면 런타임 오류라고 책에도 분명히 나와요.

강한 캡처는 크래시가 나지 않아요. 대신 더 조용하게 나빠요. 타이머가 블록을 쥐고 있는 동안 컨트롤러가 사라질 수 없으니, deinit에 넣어 둔 invalidate()가 영영 실행되지 않고 타이머는 계속 돌아요.

제대로 되는 건 weak인데, 그래도 정할 게 남아요. 이 콜백이 소유자를 계속 살려 둘 이유가 있나요? 없어요. 그럼 소유자 없는 틱은 무슨 뜻일까요? 여기서는 아무 뜻도 없고 타이머가 멈춰야 해요. 그래서 deinit이 invalidate()를 부르고, guard let self else { return }은 값싼 확인으로 남겨 둬요.

이어지는 대화 2

서로의 근거를 따라
  1. 고유 링크

    틱 문제는 weak 캡처로 해결돼요. 컨트롤러의 deinit이 실행되고 거기서 invalidate()가 타이머를 멈추니까, 화면을 닫은 뒤에는 더 울리지 않아요.

    아직 아무도 안 본 건 스레드예요. deinit은 마지막 강한 참조가 풀리는 곳에서 실행되고, invalidate()는 타이머를 가진 런 루프의 스레드에서 불러야 해요. 컨트롤러가 늘 메인 스레드에서만 해제된다면 잘 돌아가지만, 그건 코드가 약속한 게 아니라 환경이 운 좋은 거예요. 다른 스레드에서 누가 마지막 참조를 놓을 때만 문제가 드러날 테니까요.

  2. 고유 링크

    짚고 넘어갈 점은, 타이머를 Task.sleep을 도는 Task 루프로 바꿔도 같은 일이 생기고 그쪽이 더 놓치기 쉽다는 거예요. 구조화되지 않은 태스크는 부모가 없어서 컨트롤러가 사라져도 아무도 취소하지 않아요. 클로저가 self를 강하게 잡으면 루프가 컨트롤러를 살려 두고, deinit은 취소할 기회조차 얻지 못해요. [weak self]는 반복마다 안에서 강한 참조를 잡을 때만 도움이 돼요. 루프 위에 둔 guard let self는 루프 내내 붙잡고 있어요. Kotlin에서는 스코프에서 띄운 코루틴이 그 스코프와 함께 취소되는데, Swift 쪽에서 자꾸 아쉬운 게 그 부분이에요. 핸들을 들고 있다가 deinit에서 cancel()을 부르는 것 말고, 구조화되지 않은 Task를 객체 수명에 묶는 방법이 있을까요?

같은 질문의 다른 갈래

  1. 댕글링 포인터 경고는 사라졌고 포인터는 남았다

    래퍼를 통해 C++ 라이브러리에 이름을 넘기는데, 처음엔 한 줄로 CString::new(name).unwrap().as_ptr()라고 썼어요. rustc가 바로 임시 CString이 문장 끝에서 사라지니 포인터가 댕글링이…

    답글 1rustcpp
  2. getline은 실패해도 버퍼를 호출자에게 남긴다

    POSIX getline은 넘겨받은 포인터를 통해 버퍼를 할당하거나 키우며, 그 버퍼는 첫 호출부터 호출자의 것입니다. -1을 반환하는 호출도 예외가 아닙니다. 명세는 해제를 애플리케이션에 맡기고, Linux 매뉴얼 페이지는…

    답글 4c
  3. 같은 인스턴스를 가리키는 인터페이스 참조와 객체 참조

    오래된 Delphi 코드에서 제가 가장 자주 만나는 수명 실수는 한 인스턴스에 두 가지 소유 규칙을 섞는 데서 나옵니다. TInterfacedObject를 상속한 클래스는 인터페이스 참조 카운트가 0이 될 때 해제됩니다. 클…