Swift Langage Guide

[iOS / Swift] Memory Safety

Minny27 2022. 8. 26. 00:19

Memory Safety(메모리 안전)

By default, Swift prevents unsafe behavior from happening in your code. For example, Swift ensures that variables are initialized before they’re used, memory isn’t accessed after it’s been deallocated, and array indices are checked for out-of-bounds errors.

기본적으로 Swift는 코드에서 안전하지 않은 동작이 발생하는 것을 방지합니다. 예를 들어, Swift는 변수가 사용되기 전에 초기화되고할당이 해제된 후 메모리에 액세스되지 않으며, 범위를 벗어난 에러에 대해 배열 인덱스가 검사되는지 확인합니다.

 

Swift also makes sure that multiple accesses to the same area of memory don’t conflict, by requiring code that modifies a location in memory to have exclusive access to that memory. Because Swift manages memory automatically, most of the time you don’t have to think about accessing memory at all. However, it’s important to understand where potential conflicts can occur, so you can avoid writing code that has conflicting access to memory. If your code does contain conflicts, you’ll get a compile-time or runtime error.

또한 Swift는 메모리의 위치를 수정하는 코드가 해당 메모리에 독점적으로 액세스할 수 있도록 요구함으로써 동일한 메모리 영역에 대한 다중 액세스가 충돌하지 않도록 합니다. Swift는 메모리를 자동으로 관리하기 때문에 대부분의 경우 메모리 액세스에 대해 전혀 생각할 필요가 없습니다. 그러나 잠재적인 충돌이 발생할 수 있는 위치를 이해하는 것이 중요하므로 메모리에 대한 액세스 충돌이 있는 코드 작성을 피할 수 있습니다. 코드에 충돌이 포함되어 있으면 컴파일 시간 또는 런타임 오류가 발생합니다.

 

Understanding Conflicting Access to Memory(메모리에 대한 충돌 액세스 이해)

Access to memory happens in your code when you do things like set the value of a variable or pass an argument to a function. For example, the following code contains both a read access and a write access:

변수 값을 설정하거나 함수에 인자를 전달하는 것과 같은 작업을 수행할 때, 코드에서 메모리에 대한 액세스가 발생합니다. 예를 들어 다음 코드에는 읽기 액세스와 쓰기 액세스가 모두 포함되어 있습니다.

// 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)!")

A conflicting access to memory can occur when different parts of your code are trying to access the same location in memory at the same time. Multiple accesses to a location in memory at the same time can produce unpredictable or inconsistent behavior. In Swift, there are ways to modify a value that span several lines of code, making it possible to attempt to access a value in the middle of its own modification.

메모리에 대한 액세스 충돌은 코드의 다른 부분이 동시에 메모리의 동일한 위치에 액세스하려고 할 때 발생할 수 있습니다. 동시에 메모리의 한 위치에 여러 번 액세스하면 예측할 수 없거나 일관성이 없는 동작이 발생할 수 있습니다. Swift에는 여러 줄의 코드에 걸쳐 있는 값을 수정하는 방법이 있습니다. 이를 통해 자체 수정 중에 값에 액세스를 시도할 수 있습니다.

 

You can see a similar problem by thinking about how you update a budget that’s written on a piece of paper. Updating the budget is a two-step process: First you add the items’ names and prices, and then you change the total amount to reflect the items currently on the list. Before and after the update, you can read any information from the budget and get a correct answer, as shown in the figure below.

종이에 적힌 예산을 어떻게 업데이트할지 생각해 보면 비슷한 문제를 볼 수 있습니다. 예산 업데이트는 두 단계 프로세스입니다. 먼저 항목의 이름과 가격을 추가한 다음 현재 목록에 있는 항목을 반영하도록 총액을 변경합니다. 업데이트 전후에는 아래 그림과 같이 예산에서 모든 정보를 읽고 정답을 얻을 수 있습니다.

While you’re adding items to the budget, it’s in a temporary, invalid state because the total amount hasn’t been updated to reflect the newly added items. Reading the total amount during the process of adding an item gives you incorrect information.

예산에 항목을 추가하는 동안 총 금액이 새로 추가된 항목을 반영하도록 업데이트되지 않았기 때문에 예산은 일시적이고 잘못된 상태입니다. 항목을 추가하는 과정에서 총 금액을 읽으면 잘못된 정보를 제공합니다.



This example also demonstrates a challenge you may encounter when fixing conflicting access to memory: There are sometimes multiple ways to fix the conflict that produce different answers, and it’s not always obvious which answer is correct. In this example, depending on whether you wanted the original total amount or the updated total amount, either $5 or $320 could be the correct answer. Before you can fix the conflicting access, you have to determine what it was intended to do.

이 예는 또한 충돌하는 메모리 액세스를 수정할 때 직면할 수 있는 문제를 보여줍니다. 충돌을 수정하는 여러 가지 방법으로 다른 답변이 생성되는 경우가 있으며 어떤 답변이 올바른지 항상 명확하지 않습니다. 이 예에서 원래 총 금액을 원하는지 아니면 업데이트된 총 금액을 원하는지에 따라 $5 또는 $320가 정답이 될 수 있습니다. 충돌하는 액세스를 수정하기 전에 의도한 작업을 결정해야 합니다.

더보기

NOTE

If you’ve written concurrent or multithreaded code, conflicting access to memory might be a familiar problem. However, the conflicting access discussed here can happen on a single thread and doesn’t involve concurrent or multithreaded code.

동시 또는 다중 스레드 코드를 작성했다면 메모리에 대한 액세스 충돌이 익숙한 문제일 수 있습니다. 그러나 여기에서 설명하는 충돌 액세스는 단일 스레드에서 발생할 수 있으며 동시 또는 다중 스레드 코드를 포함하지 않습니다.



If you have conflicting access to memory from within a single thread, Swift guarantees that you’ll get an error at either compile time or runtime. For multithreaded code, use Thread Sanitizer to help detect conflicting access across threads.

단일 스레드 내에서 충돌하는 메모리 액세스가 있는 경우 Swift는 컴파일 시간이나 런타임에 오류가 발생하도록 보장합니다. 다중 스레드 코드의 경우 Thread Sanitizer를 사용하여 스레드 간에 충돌하는 액세스를 감지할 수 있습니다.

 

Characteristics of Memory Access(메모리 액세스의 특성)

There are three characteristics of memory access to consider in the context of conflicting access: whether the access is a read or a write, the duration of the access, and the location in memory being accessed. Specifically, a conflict occurs if you have two accesses that meet all of the following conditions:

  • At least one is a write access or a nonatomic access.
  • They access the same location in memory.
  • Their durations overlap.

액세스가 충돌하는 상황에서 고려해야 할 메모리 액세스의 세 가지 특성이 있습니다. 액세스가 읽기인지 쓰기인지, 액세스 기간 및 액세스되는 메모리의 위치입니다. 특히 다음 조건을 모두 충족하는 두 개의 액세스 권한이 있는 경우 충돌이 발생합니다.

  • 적어도 하나는 쓰기 액세스 또는 비원자 액세스입니다.
  • 메모리의 동일한 위치에 액세스합니다.
  • 기간이 겹칩니다.

 

The difference between a read and write access is usually obvious: a write access changes the location in memory, but a read access doesn’t. The location in memory refers to what is being accessed—for example, a variable, constant, or property. The duration of a memory access is either instantaneous or long-term.

읽기 액세스와 쓰기 액세스의 차이점은 일반적으로 분명합니다. 쓰기 액세스는 메모리의 위치를 ​​변경하지만 읽기 액세스는 변경하지 않습니다. 메모리의 위치는 액세스 중인 항목(예: 변수, 상수 또는 프로퍼티)을 나타냅니다. 메모리 액세스 기간은 순간적이거나 장기적입니다.

 

An operation is atomic if it uses only C atomic operations; otherwise it’s nonatomic. For a list of those functions, see the stdatomic(3) man page.

C 원자적 연산만 사용하는 경우 연산은 원자적입니다. 그렇지 않으면 비원자적입니다. 이러한 함수 목록은 stdatomic(3) 매뉴얼 페이지를 참조하십시오.



An access is instantaneous if it’s not possible for other code to run after that access starts but before it ends. By their nature, two instantaneous accesses can’t happen at the same time. Most memory access is instantaneous. For example, all the read and write accesses in the code listing below are instantaneous:

액세스가 시작된 후 종료되기 전에 다른 코드를 실행할 수 없는 경우 액세스는 즉각적입니다. 본질적으로 두 개의 즉각적인 액세스는 동시에 발생할 수 없습니다. 대부분의 메모리 액세스는 즉각적입니다. 예를 들어 아래 코드 목록의 모든 읽기 및 쓰기 액세스는 즉각적입니다.

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

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

However, there are several ways to access memory, called long-term accesses, that span the execution of other code. The difference between instantaneous access and long-term access is that it’s possible for other code to run after a long-term access starts but before it ends, which is called overlap. A long-term access can overlap with other long-term accesses and instantaneous accesses.

그러나 다른 코드 실행에 걸쳐 메모리에 액세스하는 여러 가지 방법이 있습니다. 이를 장기 액세스라고 합니다. 즉시 액세스와 장기 액세스의 차이점은 장기 액세스가 시작된 후 종료되기 전에 다른 코드가 실행될 수 있다는 점입니다. 이를 오버랩이라고 합니다. 장기 액세스는 다른 장기 액세스 및 순간 액세스와 중첩될 수 있습니다.



Overlapping accesses appear primarily in code that uses in-out parameters in functions and methods or mutating methods of a structure. The specific kinds of Swift code that use long-term accesses are discussed in the sections below.

중복 액세스는 주로 함수 및 메서드에서 in-out 매개변수를 사용하거나 구조체의 메서드를 변경(mutating)하는 코드에서 나타납니다. 장기 액세스를 사용하는 특정 종류의 Swift 코드는 아래 섹션에서 설명합니다.

 

Conflicting Access to In-Out Parameters(In-Out 매개변수에 대한 액세스 충돌)

A function has long-term write access to all of its in-out parameters. The write access for an in-out parameter starts after all of the non-in-out parameters have been evaluated and lasts for the entire duration of that function call. If there are multiple in-out parameters, the write accesses start in the same order as the parameters appear.

함수는 모든 in-out 매개변수에 대한 장기 쓰기 액세스 권한을 가집니다. in-out 매개변수에 대한 쓰기 액세스는 모든 non-in-out 매개변수가 평가된 후 시작되고 해당 함수 호출의 전체 기간 동안 지속됩니다. in-out 매개변수가 여러 개인 경우 쓰기 액세스는 매개변수가 나타나는 순서대로 시작됩니다.



One consequence of this long-term write access is that you can’t access the original variable that was passed as in-out, even if scoping rules and access control would otherwise permit it—any access to the original creates a conflict. For example:

이 장기 쓰기 액세스의 결과 중 하나는 범위 지정 규칙과 액세스 제어가 허용하더라도 in-out으로 전달된 원래 변수에 액세스할 수 없다는 것입니다. 원본에 대한 액세스는 충돌을 일으킵니다. 예를 들어:

var stepSize = 1

func increment(_ number: inout Int) {
    number += stepSize
}

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

In the code above, stepSize is a global variable, and it’s normally accessible from within increment(_:). However, the read access to stepSize overlaps with the write access to number. As shown in the figure below, both number and stepSize refer to the same location in memory. The read and write accesses refer to the same memory and they overlap, producing a conflict.

위의 코드에서 stepSize는 전역 변수이며 일반적으로 increment(_:) 내에서 액세스할 수 있습니다. 그러나 stepSize에 대한 읽기 액세스는 number에 대한 쓰기 액세스와 겹칩니다. 아래 그림과 같이 number와 stepSize는 모두 메모리의 동일한 위치를 참조합니다. 읽기 및 쓰기 액세스는 동일한 메모리를 참조하며 중복되어 충돌이 발생합니다.

One way to solve this conflict is to make an explicit copy of stepSize:

이 충돌을 해결하는 한 가지 방법은 stepSize의 명시적 복사본을 만드는 것입니다.

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

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

When you make a copy of stepSize before calling increment(_:), it’s clear that the value of copyOfStepSize is incremented by the current step size. The read access ends before the write access starts, so there isn’t a conflict.

increment(_:)를 호출하기 전에 stepSize의 복사본을 만들 때 copyOfStepSize의 값이 현재 단계 크기만큼 증가하는 것이 분명합니다. 읽기 접근은 쓰기 접근이 시작되기 전에 끝나므로 충돌이 없습니다.



Another consequence of long-term write access to in-out parameters is that passing a single variable as the argument for multiple in-out parameters of the same function produces a conflict. For example:

in-out 매개변수에 대한 장기 쓰기 액세스의 또 다른 결과는 동일한 함수의 여러 in-out 매개변수에 대한 인수로 단일 변수를 전달하면 충돌이 발생한다는 것입니다. 예를 들어:

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

The balance(_:_:) function above modifies its two parameters to divide the total value evenly between them. Calling it with playerOneScore and playerTwoScore as arguments doesn’t produce a conflict—there are two write accesses that overlap in time, but they access different locations in memory. In contrast, passing playerOneScore as the value for both parameters produces a conflict because it tries to perform two write accesses to the same location in memory at the same time.

위의 balance(_:_:) 함수는 두 매개변수를 수정하여 전체 값을 균등하게 나눕니다. playerOneScore 및 playerTwoScore를 인수로 사용하여 호출하면 충돌이 발생하지 않습니다. 시간적으로 겹치는 두 개의 쓰기 액세스가 있지만 메모리의 다른 위치에 액세스합니다. 대조적으로 두 매개변수의 값으로 playerOneScore를 전달하면 메모리의 동일한 위치에 대해 동시에 두 개의 쓰기 액세스를 수행하려고 하기 때문에 충돌이 발생합니다.

더보기

NOTE

Because operators are functions, they can also have long-term accesses to their in-out parameters. For example, if balance(_:_:) was an operator function named <^>, writing playerOneScore <^> playerOneScore would result in the same conflict as balance(&playerOneScore, &playerOneScore).

연산자는 함수이기 때문에 in-out 매개변수에 장기간 액세스할 수도 있습니다. 예를 들어, balance(_:_:)가 <^>라는 연산자 함수인 경우 playerOneScore <^> playerOneScore를 작성하면 balance(&playerOneScore, &playerOneScore)와 동일한 충돌이 발생합니다.

 

Conflicting Access to self in Methods(메서드에서 self에 대한 충돌 액세스)

A mutating method on a structure has write access to self for the duration of the method call. For example, consider a game where each player has a health amount, which decreases when taking damage, and an energy amount, which decreases when using special abilities.

구조체의 mutating 메서드는 메서드 호출 기간 동안 자체에 대한 쓰기 액세스 권한을 가집니다. 예를 들어, 각 플레이어가 피해를 입을 때 감소하는 건강 양이 있고 특수 능력을 사용할 때 감소하는 에너지 양이 있는 게임을 고려하십시오.

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

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

In the restoreHealth() method above, a write access to self starts at the beginning of the method and lasts until the method returns. In this case, there’s no other code inside restoreHealth() that could have an overlapping access to the properties of a Player instance. The shareHealth(with:) method below takes another Player instance as an in-out parameter, creating the possibility of overlapping accesses.

위의 restoreHealth() 메서드에서 self에 대한 쓰기 액세스는 메서드의 시작 부분에서 시작하여 메서드가 반환될 때까지 지속됩니다. 이 경우 플레이어 인스턴스의 프로퍼티에 중복 액세스할 수 있는 restoreHealth() 내부에 다른 코드가 없습니다. 아래의 shareHealth(with:) 메서드는 다른 Player 인스턴스를 in-out 매개변수로 사용하여 액세스가 겹칠 가능성을 만듭니다.

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

In the example above, calling the shareHealth(with:) method for Oscar’s player to share health with Maria’s player doesn’t cause a conflict. There’s a write access to oscar during the method call because oscar is the value of self in a mutating method, and there’s a write access to maria for the same duration because maria was passed as an in-out parameter. As shown in the figure below, they access different locations in memory. Even though the two write accesses overlap in time, they don’t conflict.

위의 예에서 Oscar의 플레이어가 Maria의 플레이어와 건강을 공유하도록 shareHealth(with:) 메서드를 호출해도 충돌이 발생하지 않습니다. oscar는 mutating 메서드에서 self의 값이기 때문에 메서드 호출 중에 oscar에 대한 쓰기 액세스가 있고, Maria가 in-out 매개변수로 전달되었기 때문에 동일한 기간 동안 Maria에 대한 쓰기 액세스가 있습니다. 아래 그림과 같이 메모리의 다른 위치에 액세스합니다. 두 쓰기 액세스가 시간적으로 겹치더라도 충돌하지 않습니다.

However, if you pass oscar as the argument to shareHealth(with:), there’s a conflict:

그러나 shareHealth(with:)의 인수로 oscar를 전달하면 충돌이 발생합니다.

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

The mutating method needs write access to self for the duration of the method, and the in-out parameter needs write access to teammate for the same duration. Within the method, both self and teammate refer to the same location in memory—as shown in the figure below. The two write accesses refer to the same memory and they overlap, producing a conflict.

mutating 메소드는 메소드 기간 동안 self에 대한 쓰기 액세스가 필요하고 in-out 매개변수는 동일한 기간 동안 teammate에 대한 쓰기 액세스가 필요합니다. 메서드 내에서 자신과 팀원 모두 아래 그림과 같이 메모리의 동일한 위치를 참조합니다. 두 개의 쓰기 액세스는 동일한 메모리를 참조하고 겹치므로 충돌이 발생합니다.

 

Conflicting Access to Properties(프로퍼티에 대한 액세스 충돌)

Types like structures, tuples, and enumerations are made up of individual constituent values, such as the properties of a structure or the elements of a tuple. Because these are value types, mutating any piece of the value mutates the whole value, meaning read or write access to one of the properties requires read or write access to the whole value. For example, overlapping write accesses to the elements of a tuple produces a conflict:

구조체, 튜플 및 열거형와 같은 타입은 구조체의 프로퍼티이나 튜플의 요소와 같은 개별 구성 값으로 구성됩니다. 이들은 값 타입이기 때문에 값의 일부를 변경하면 전체 값이 변경됩니다. 즉, 프로퍼티 중 하나에 대한 읽기 또는 쓰기 액세스에는 전체 값에 대한 읽기 또는 쓰기 액세스가 필요합니다. 예를 들어, 튜플의 요소에 대한 중복 쓰기 액세스는 충돌을 생성합니다.

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

In the example above, calling balance(_:_:) on the elements of a tuple produces a conflict because there are overlapping write accesses to playerInformation. Both playerInformation.health and playerInformation.energy are passed as in-out parameters, which means balance(_:_:) needs write access to them for the duration of the function call. In both cases, a write access to the tuple element requires a write access to the entire tuple. This means there are two write accesses to playerInformation with durations that overlap, causing a conflict.

위의 예에서 튜플의 요소에 대해 balance(_:_:)를 호출하면 playerInformation에 대한 중복 쓰기 액세스가 있기 때문에 충돌이 발생합니다. playerInformation.health와 playerInformation.energy는 모두 in-out 매개변수로 전달됩니다. 즉, balance(_:_:)는 함수 호출 기간 동안 이에 대한 쓰기 액세스 권한이 필요합니다. 두 경우 모두 튜플 요소에 대한 쓰기 액세스에는 전체 튜플에 대한 쓰기 액세스 권한이 필요합니다. 이는 플레이어 정보에 대한 두 개의 쓰기 액세스가 중복되는 지속 시간으로 인해 충돌을 일으키고 있음을 의미합니다.

 

The code below shows that the same error appears for overlapping write accesses to the properties of a structure that’s stored in a global variable.

아래 코드는 전역 변수에 저장된 구조체의 프로퍼티에 대한 중복 쓰기 액세스에 대해 동일한 오류가 나타나는 것을 보여줍니다.

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

In practice, most access to the properties of a structure can overlap safely. For example, if the variable holly in the example above is changed to a local variable instead of a global variable, the compiler can prove that overlapping access to stored properties of the structure is safe:

실제로 구조체 프로퍼티에 대한 대부분의 액세스는 안전하게 겹칠 수 있습니다. 예를 들어 위의 예에서 변수 holly가 전역 변수 대신 지역 변수로 변경되면 컴파일러는 구조체의 저장 프로퍼티에 대한 중복 액세스가 안전하다는 것을 증명할 수 있습니다.

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

In the example above, Oscar’s health and energy are passed as the two in-out parameters to balance(_:_:). The compiler can prove that memory safety is preserved because the two stored properties don’t interact in any way.

위의 예에서 Oscar의 건강과 에너지는 balance(_:_:)에 대한 두 개의 in-out 매개변수로 전달됩니다. 컴파일러는 두 개의 저장 프로퍼티가 어떤 식으로든 상호 작용하지 않기 때문에 메모리 안전이 유지됨을 증명할 수 있습니다.

 

The restriction against overlapping access to properties of a structure isn’t always necessary to preserve memory safety.Memory safety is the desired guarantee, but exclusive access is a stricter requirement than memory safety-which means some code preserves memory safety, even though it violates exclusive access to memory. Swift allows this memory-safe code if the compiler can prove that the nonexclusive access to memory is still safe. Specifically, it can prove that overlapping access to properties of a structure is safe if the following conditions apply:

메모리 안전을 유지하기 위해 구조체 프로퍼티에 대한 중복 액세스에 대한 제한이 항상 필요한 것은 아닙니다. 메모리 안전은 원하는 보장이지만 배타적 액세스는 메모리 안전보다 더 엄격한 요구 사항입니다. 즉, 일부 코드는 메모리에 대한 배타적 액세스를 위반하더라도 메모리 안전을 유지합니다. 컴파일러가 메모리에 대한 비독점적 액세스가 여전히 안전하다는 것을 증명할 수 있는 경우 Swift는 이 메모리 안전 코드를 허용합니다. 특히 다음 조건이 적용되는 경우 구조채 프로퍼티에 대한 중복 액세스가 안전하다는 것을 증명할 수 있습니다.

 

You’re accessing only stored properties of an instance, not computed properties or class properties.

  • The structure is the value of a local variable, not a global variable.
  • The structure is either not captured by any closures, or it’s captured only by nonescaping closures.
  • If the compiler can’t prove the access is safe, it doesn’t allow the access.

연산 프로퍼티나 클래스 타입이 아닌 인스턴스의 저장된 프로퍼티에만 액세스하고 있습니다.

  • 구조체는 전역 변수가 아니라 지역 변수의 값입니다.
  • 구조체는 클로저에 의해 캡처되지 않거나 nonescaping 클로저에만 캡처됩니다.
  • 컴파일러가 액세스가 안전한지 증명할 수 없으면 액세스를 허용하지 않습니다.

 

 

 

※ 참고 출처

애플 개발자 문서

 

Memory Safety — The Swift Programming Language (Swift 5.7)

Memory Safety By default, Swift prevents unsafe behavior from happening in your code. For example, Swift ensures that variables are initialized before they’re used, memory isn’t accessed after it’s been deallocated, and array indices are checked for

docs.swift.org