오늘은 메모리 충돌 관련한 Swift 공식 문서를 읽어보고 내용 정리해 보는 시간을 가져볼게요.
# 개요
Swift는 동일한 영역에 있는 메모리에 대해서 여러 액세스가 충돌하지 않도록 하기 위해 메모리 위치를 수정(=write)하는 코드가 해당 메모리에 대해 독점적인 액세스 권한을 갖는 것을 보장합니다.
즉, 메모리에 write 액세스를 해야 하는 경우 read든 write든 다른 액세스가 올 수 없다는 거죠.
만약 충돌이 발생하면 컴파일 타임 에러 또는 런타임 에러가 발생해요.
언제 충돌이 발생할 수 있는지를 알면 충돌이 발생하지 않는 코드를 작성할 수 있겠죠??ㅎㅎ
알아봅시다.
[참고]
앞으로 설명드리는 얘기는 단일 스레드 환경입니다.
단일 스레드에서 메모리에 대해 액세스가 충돌한다면, Swift에서 컴파일 타임 또는 런타임 때 에러가 발생할 것을 보장합니다.
또한, 다중 스레드의 경우 Thread Sanitizer를 사용하여 스레드 간 액세스 충돌을 감지할 수 있습니다.
# 충돌이 발생하는 조건
아래 조건을 모두 만족하는 경우 충돌이 발생합니다.
[충돌이 발생하는 조건]
(1) 최소한 하나가 write 액세스이거나 nonaotmic 액세스인 경우
(2) 두 액세스가 동일한 메모리 위치에 액세스하는 경우
(3) 액세스하는 기간이 겹치는 경우
# 액세스
액세스는 크게 instantaneous(즉시) 액세스와 long-term(장기) 액세스로 구분됩니다.
instantaneous 액세스의 특징은 해당 액세스가 시작된 후에 다른 코드가 실행될 수 없으며 2개의 instantaneous 액세스가 동시에 발생할 수 없다는 것입니다.
아래는 write와 read에 대한 instantaneous 액세스 예시입니다.
그러나, long-term 액세스는 액세스가 시작된 후에 다른 코드가 실행될 수 있어요. 이걸 overlap(중첩)이라고 부릅니다.
즉, long-term 액세스는 다른 long-term 및 instantaneous 액세스와 overlap이 가능합니다.
[참고] overlap과 충돌은 다른거에요!
overlap은 같은 메모리에 대해 액세스가 동시에 중첩되는 상황을 말하는 것이고, overlap 되는 상황 중에서 충돌이 발생할 수도 있고 발생 안 할 수도 있는 것입니다.
overlap 되는 상황은 주로 구조체의 mutating 메서드나 in-out 매개변수에서 발생해요.
# in-out 파라미터로 인한 충돌
모든 in-out 파라미터에 대해서 함수는 함수가 끝날 때까지 long-term write 액세스를 가져요.
(만약 in-out 파라미터가 여러 개 있는 경우, write 액세스는 파라미터가 나타나는 순서대로 시작됩니다.)
예를 들어볼게요.
위 코드를 보면, 전역 변수인 stepSize를 inout 파라미터로 전달했기 때문에 write 액세스가 시작되지만, 함수 안에선 read 액세스도 시작됩니다.
write 액세스와 read 액세스가 동일한 메모리에 대해 overlap 되었기 때문에 충돌이 발생합니다.
explicit copy를 활용하면 충돌을 해결할 수 있어요.
# mutating 메서드로 인한 충돌
구조체의 mutating 메서드는 메서드 호출 전체 기간 동안 self에 대해 write 액세스를 가집니다.
아래 코드를 볼까요?
mutating 메서드라서 self에 대한 write 액세스가 시작되었는데요. 그러나 그 이후에 self에 대해 overlap이 될만한 액세스가 없었기 때문에 충돌이 발생하지 않습니다.
이번엔 mutating과 in-out 파라미터를 같이 사용한 예시를 볼게요.
위 코드는 충돌이 발생하지 않아요.
mutating 메서드로 인해 Oscar에 대한 write 액세스가 발생하고, in-out 파라미터로 인해 Maria에 대한 write 액세스가 발생합니다.
그러나 두 액세스는 아래처럼 서로 다른 메모리에 대해 액세스가 이뤄졌기 때문에 overlap은 됐지만 충돌은 발생하지 않습니다.
하지만 만약 아래처럼 구현을 한다면 Oscar에 대한 write 액세스가 overlap 되기 때문에 충돌이 발생하게 됩니다.
# property로 인한 충돌
구조체, 튜플, enum 같은 타입들은 개별 구성 값으로 이루어져 있는데요.
(ex. 구조체의 property, 튜플의 element)
이런 타입은 value type이기 때문에 값이 변경되면 전체 값이 변경됩니다.
즉, 하나의 속성에 대한 write 또는 read 액세스는 전체 값에 대한 write 또는 read 액세스를 필요로 합니다.
예를 들어서, 아래처럼 튜플 각 element에 대한 write 액세스는 튜플 전체에 대한 write 액세스가 overlap 되기 때문에 충돌이 발생합니다.
구조체의 경우, 전역 변수에 저장된 구조체의 property에 대한 write 액세스도 충돌이 발생합니다.
하지만 전역 변수 대신 지역 변수를 사용한다면, 컴파일러는 overlap이 발생하더라도 안전하다고 판단하기 때문에 문제가 없어요.
(그 이유는 지역 변수가 해당 변수의 scope 바깥에서는 어떤 상호 작용하지 않는다는 것을 알기 때문이에요.)
다음 조건이 해당되면 구조체 property에 대해 액세스가 overlap 되더라도 안전하다고 증명할 수 있습니다.
[구조체 property에 대해 액세스가 overlap 되더라도 안전한 조건]
(1) 인스턴스의 stored property만 액세스하고 computed property나 class property에 액세스 하지 않는 경우
(2) 구조체가 지역 변수인 경우
(3) 구조체가 어떤 클로저에도 capture 되지 않거나, non-escaping 클로저에만 capture 되는 경우
# 참고
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/memorysafety/
이번 글은 여기서 마무리.
'Swift' 카테고리의 다른 글
@discardableResult (0) | 2024.01.28 |
---|---|
KeyValuePairs (0) | 2024.01.28 |
클래스(class type) 생성자에 대해 알아보자 (0) | 2024.01.21 |
구조체(value type) 생성자에 대해 알아보자 (0) | 2024.01.17 |
private(set) (0) | 2024.01.17 |