티스토리 뷰

728x90

Swift는 코드가 불안전한 동작 발생을 방지합니다.

예를들어, 변수가 사용되기 전에 초기화 된다 거나, 할당이 해제 된 후 메모리에 접근하지 않도록 하거나, 배열 인덱스의 범위를 넘는 등이 있습니다. 이렇듯 Swift에서 메모리 관련해 자동으로 관리해 주기때문에 대부분의 경우에는 Swift언어를 사용하는 사용자는 메모리의 접근에 대해 전혀 생각하지 않고 사용해도 됩니다.

하지만 메모리 접근 충돌이 발생할 수 있는 상황을 이해하고 메모리 접근 충돌을 피하는 코드를 어떻게 작성할 수 있는지 이해하는 것은 중요합니다.

메모리 접근 충돌이 일어나면 런타임 에러, 컴파일 에러가 발생 합니다.

 


메모리 접근 충돌의 이해 (Understanding Conflicting Access to Memory)

 

아래 예시를 보면

// A write access to the memory where one is stored.
var one = 1

// A read access from the memory where one is stored.
print("We're number \(one)!")

one 이라는 변수에 1을 할당 (쓰기)

print 하기 위해 one에 접근 (읽기)

 

메모리 충돌은 메모리에 값 할당과 메모리 값 접근동시에 수행할때 발생합니다.

 

예를들어

아래와 같이 물건을 구매하고 총 금액을 확인하는 경우 Before 상태의 Total은 $5 입니다.

만약 TV, T-shirt를 구매하는동안 (During) Total에 접근해 값을 가져온다면 Total은 그대로 $5가 됩니다.

원하는 값은 TV와 T-shirt를 다 포함한 $320이 되야하는데 말이죠.

 

 

NOTE

만약 동시성(concurrent) 코드나, 멀티쓰레드 코드를 작성한적이 있다면 이 메모리 접근 충돌 문제는 익숙할 겁니다.
하지만 이 접근 충돌 문제는 싱글쓰레드에서 발생할 수 있는 문제이고 동시성과 멀티쓰레드와는 연관이 없습니다. 

 

 

 

메모리 접근의 특징 (Characteristics of Memory Access)

 

충돌 접근에서 고려해야할 메모리 접근의 3가지 특성이 있습니다.

접근 하는동안 읽기 또는 쓰기 접근인지 여부와 접근된 메모리의 위치입니다.

특히 아래의 조건을 모두 만족하는 2개의 접근이 있다면 충돌이 발생합니다.

 

  • 최소 하나의 쓰기 접근 상황 또는 비원자적 접근
  • 메모리의 같은 위치에 접근
  • 접근하는 시간이 겹침

읽기와 쓰기 접근 차이는 아래와 같습니다.

 

쓰기 접근: 메모리 위치 변경

읽기 접근: 메모리 위치 변경 x

 

메모리 접근기간은 즉각접근(instantaneous)과 장기접근(long-term)으로 구분할 수 있습니다.

즉각접근(instantaneous) 장기접근(long-term)
코드에서 메모리 접근이 시작되고 종료전에
다른코드를 실행할 수 없는경우
다른 메모리들이 접근 가능

 

아래는 즉각접근에 대한 예시 입니다.

  func oneMore(than number: Int) -> Int {
      return number + 1
  }

  // 1
  var myNumber = 1  
  // 3         // 2
  myNumber = oneMore(than: myNumber) 
  print(myNumber)
  // Prints "2"

메모리 접근 충돌 하지 않음.

 


In-Out 파라미터에 충돌 접근 (Conflicting Access to In-Out Parameters)

 

메모리 충돌은 in-out 파라미터를 잘못 사용할 때 발생합니다.

var stepSize = 1

func increment(_ number: inout Int) { 
    // number: 쓰기
    // stepSize: 읽기
    number += stepSize  // 쓰기/읽기가 동시에 발생하므로 충돌!
}

increment(&stepSize)
// Error: conflicting accesses to stepSize

위 처럼 inout 파라미터 number를 읽고, number + stepSize의 값을 number에 할당하면서 

동시에 쓰기와 읽기가 발생하게되서 접근충돌이 일어납니다.

 

 

충돌 해결방안

stepSize의 복사본을 명시적으로 사용합니다.

// Make an explicit copy.
var copyOfStepSize = stepSize
increment(&copyOfStepSize)

// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2

stepSize를 복사한 copyOfStepSize를 사용하여 하나의 메모리를 읽고 쓰는 행위를 동시에 하지 않으므로 접근 충돌을 피할 수 있습니다.

 

 

 

또 다른 장기 접근 충돌 

아래는 balance라는 메서드에 inout파라미터를 2개 사용할때의 예시 입니다.

func balance(_ x: inout Int, _ y: inout Int) {
    let sum = x + y  
    x = sum / 2  // 질문: 이 부분에서 동시 발생?
    y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore)  // OK
balance(&playerOneScore, &playerOneScore)
// Error: conflicting accesses to playerOneScore

 

마지막 줄의 balance(&playerOneScore, &playerOneScore) 는 읽기와 쓰기를 동시에 하게 돼, 접근 충돌이 발생합니다.

 


메서드에서 self에 충돌 접근 (Conflicting Access to self in Methods)

 

구조체의 mutating func는 메서드 안에서 self에 접근할때 충돌이 발생할 수도 있습니다.

struct Player {
    var name: String
    var health: Int
    var energy: Int

    static let maxHealth = 10
    mutating func restoreHealth() {
        health = Player.maxHealth
    }
}

위의 restoreHealth() 메서드 내부에 Player인스턴스 프로퍼티에 중복접근하는 코드가 없으므로 충돌이 없습니다.

 

위 구조체 확장을 통해 Player간에 체력을 공유하는 shareHealth() 함수를 선언합니다.

struct Player {
    var name: String
    var health: Int
    var energy: Int

    static let maxHealth = 10
    mutating func restoreHealth() {
        health = Player.maxHealth
    }
}

extension Player {
    mutating func shareHealth(with teammate: inout Player) {
        balance(&teammate.health, &health)
    }
}

var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria)  // OK

위 예제에서 Maria의 체력을 공유해도

teammate.health = Maria의 체력

health = oscar의 체력이기 때문에 충돌이 나지 않습니다.

 

 

하지만 아래처럼 oscar가 자신의 체력을 공유한다면 ?

oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar

체력을 읽어오고 체력을 변경하는 동작을 한 메모리 위치에서 동시에 수행하기 때문에 충돌이 발생합니다.

 


프로퍼티에서 충돌 접근 (Conflicting Access to Properties)

 

프로퍼티 접근시 충돌은 아래와 같습니다.

/* 
  func balance(_ x: inout Int, _ y: inout Int) {
    let sum = x + y 
    x = sum / 2
    y = sum - x
  }
*/

var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation

전역변수 playerInformation을 가지고 읽기/쓰기를 동시에 수행하는 balance를 실행할때 충돌이 납니다.

아래와 같이 Player의 경우에도 전역변수로 선언되어 있어서 balance 실행시 충돌이 납니다.

var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)  // Error

 

하지만 지역변수에서 balance를 실행할 경우 충돌에러가 발생하지 않습니다.

func someFunction() {
    var oscar = Player(name: "Oscar", health: 10, energy: 10)
    balance(&oscar.health, &oscar.energy)  // OK
}

 

구조체에서 프로퍼티에 접근하는데 오버래핑 접근으로부터 안전한 상황은 아래와 같습니다. 

 

  • 구조체 인스턴스에서 저장프로퍼티에만 접근할 때
  • 구조체가 전역변수가 아니라 지역변수 일때
  • 구조체가 어떤 클로저로부터도 캡처 하지않거나 nonescaping클로저에서만 획득된 경우

 

만약 컴파일러가 접근이 안전하다고 판단하지 못하면 접근 할 수 없습니다.

728x90