싱글 쓰레드 환경에서도 하나의 메모리에 동시적 접근이 발생 가능하다.
1. 메모리에 충돌하는 접근 이해
- 변수의 값을 설정하거나 함수에 인자를 전달하는 동작을 할 때 메모리에 접근하게 된다.
var one = 1 // 쓰기 접근.
print("We're number \(one)!") // 읽기 접근.
- 코드의 다른 부분이 같은 시간에 메모리의 같은 위치에 접근할 때 충돌이 발생할 수 있다.
메모리 접근의 특징
3가지 조건을 모두 만족하는 2개의 접근이 있다면 충돌이 발생한다.
- 적어도 하나는 쓰기 접근 (메모리 위치를 변경) 이거나 non-atomic (C atomic 연산이 아닌 것) 접근이다.
- 메모리의 같은 위치에 접근한다.
- 접근 시간이 겹친다. (순간적 / 장기적)
- 접근이 시작되고, 종료되기 전에 다른 코드를 실행할 수 없는 경우에는 접근은 동시에 이루어진다.
- 메모리에 2개의 즉시 접근은 일어날 수가 없다.
- 메모리에는 즉각적인 접근이 일어난다;
1 . In-Out 파라미터에 충돌 접근
- 함수는 모든 in-out 파라미터에 대해 장기 쓰기 접근 을 하게 된다.
- non-in-out 파라미터가 평가된 후에 in-out 파라미터에 쓰기 접근을 시작하고, 함수가 호출되는 동안 쓰기 접근이 지속된다.
- inout 파라미터가 여러개면 inout 파라미터가 나타나는 순서대로 메모리 쓰기 접근을 시행한다.
문제1. inout전달된 변수의 외부 변수로 직접 접근을 할 수 없다.
var stepsize = 1
// 변수 stepsize에 장기적인 쓰기 접근 (입출력 파라미터)
func increment(_ number: inout Int) {
number += stepsize // 변수 stepsize에 읽기 접근
}
increment(&stepsize) // 메모리에 동시접근으로 인한 문제 발생
inout변수로 전달된 namber변수의 외부변수인 stepsize로 직접접근할 수 없다
이 상황을 그림으로 표현하면 아래와 같다.
함수 내의 number 변수는 쓰기권한을 가지고있다. stepsize는 읽기 권한을 가지고있다. 일반적으로 메모리 공간의 동시에 쓰기와 읽기권한을 동시에 허용하지 않는다. 쓰기는 메모리의 수정권한을 가지고 있는데, 읽기는 메모리의 수정권한을 가지고 있지 않다.
두 각기 다른 권한이 충돌하며 오버랩이 발생하고 에러가 발생한다.
메모리 공간의 동시에 쓰기와 읽기권한을 허용하지 않는 이유
동시에 허용하지 않는 이유는 예측할 수 없는 동작을 유발할 수 있기 때문이다. 읽기와 쓰기의 작업이 순서가 보장되지 않는 한 경합상태가 일어날 수 있기 때문이다.
해결방법
var stepSize = 1
var copyOfStepSize = stepSize //먼저 stepSize를 읽어서 값을 복사한다.
// 변수 stepSize에 장기적인 쓰기 접근 (입출력 파라미터)
func incrementing(_ number: inout Int) {
number += stepSize //복사한 값에 쓴다. stepsize를 더한다.
}
incrementing(©OfStepSize)
stepSize = copyOfStepSize //원본을 교체한다.
increment(_:) 호출 전에 stepSize 의 복사본을 만들 때 copyOfStepSize 의 값이 현재 수만큼 증가된다는 것은 명확하다.
읽기 접근은 쓰기 접근이 시작되기 전에 끝나므로 충돌이 일어나지 않는다.
문제2. inout파라미터에 단일 변수를 전달하면 충돌이 발생한다.
func balance(_ x: inout Int, _ y: inout Int) { // 평균값 설정하는 함수
let sum = x + y
x = sum / 2
y = sum - x
}
balance(&playerOneScore, &playerOneScore) // 에러 발생 ⭐️
2. 메서드에서 self에 충돌접근
- 구조체의 mutating method 는 메서드가 호출되고 반환될때 까지 self 에 대한 쓰기 접근을 가진다.
struct Player {
var name: String
var health: Int
var energy: Int
// 타입 속성
static let maxHealth = 10
// health값을 바꾸는 메서드 (self.health에 접근)
mutating func restoreHealth() {
health = Player.maxHealth
}
}
// 확장
extension Player {
// 자신의 체력과, 동료의 체력을 공유해서 평균 설정
mutating func shareHealth(with teammate: inout Player) { // 메모리 장기적 접근
balance(&teammate.health, &health)
}
}
var maria = Player(name: "Maria", health: 10, energy: 10)
var oscar = Player(name: "Oscar", health: 5, energy: 10)
// "마리아"와 "오스카"의 체력을 공유
mario.shareHealth(with: &luigi) // OK: 오버랩은 되나 다른 주소에 접근
하지만 자신과 자신의 체력을 공유한다면? 충돌⭐️⭐️⭐️
oscar.shareHealth(with: &oscar) // Error: 단일 주소에 접근
3. 프로퍼티에 대한 접근 충돌
var playerInformation = (health: 10, energy: 20)
// 튜플에 대한 동시접근 문제
balance(&playerInformation.health, &playerInformation.energy)
전역이 아닌 "지역 변수"로 만들어서 접근하는 것은 괜찮음.. 왜?지?요?
// 전역이 아닌 "지역 변수"로 만들어서 접근하는 것은 괜찮음
func someFunction() {
var yosi2 = Player(name: "요시2", health: 10, energy: 10)
balance(&yosi2.health, &yosi2.energy) //
}
도대체 왜?
컴파일러가 안전하다는 것을 판단하는 경우
1) (계신속성 / 타입속성이 아닌) 인스턴스의 저장속성에만 접근하는 경우
2) 구조체가 전역변수가 아닌, 지역변수로만 선언될 때
3) 구조체가 클로저에 의해 캡처되지 않았거나, non-escaping클로저로만 캡처되었을 때