Unetway

Swift - Безопасность памяти

По умолчанию Swift предотвращает небезопасное поведение в вашем коде. Например, Swift гарантирует, что переменные инициализируются до их использования, память не доступна после ее освобождения, а индексы массива проверяются на наличие ошибок за пределами допустимых пределов.

Swift также гарантирует, что множественный доступ к одной и той же области памяти не конфликтует, требуя, чтобы код, изменяющий местоположение в памяти, имел эксклюзивный доступ к этой памяти. Поскольку Swift управляет памятью автоматически, большую часть времени вам вообще не нужно думать о доступе к памяти. Однако важно понимать, где могут возникнуть потенциальные конфликты, поэтому вы можете избежать написания кода, который имеет конфликтующий доступ к памяти. Если ваш код содержит конфликты, вы получите ошибку во время компиляции или во время выполнения.

Понимание конфликта доступа к памяти

Доступ к памяти происходит в вашем коде, когда вы делаете такие вещи, как установка значения переменной или передача аргумента функции. Например, следующий код содержит и права на чтение, и права на запись:

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

Конфликт доступа к памяти может возникнуть, когда разные части вашего кода пытаются получить доступ к одному и тому же месту в памяти в одно и то же время. Многократный доступ к определенному месту в памяти может привести к непредсказуемому или противоречивому поведению. В Swift есть способы изменить значение, которое занимает несколько строк кода, что позволяет попытаться получить доступ к значению в середине его собственной модификации.

Вы можете увидеть аналогичную проблему, подумав о том, как обновить бюджет, написанный на листе бумаги. Обновление бюджета представляет собой двухэтапный процесс: сначала вы добавляете названия и цены товаров, а затем меняете общую сумму, чтобы отразить товары, которые в данный момент находятся в списке. До и после обновления вы можете прочитать любую информацию из бюджета и получить правильный ответ.

 

Пока вы добавляете элементы в бюджет, он временно находится в недопустимом состоянии, поскольку общая сумма не была обновлена, чтобы отразить добавленные элементы. Считывание общей суммы в процессе добавления элемента дает вам неверную информацию.

В этом примере также демонстрируется проблема, с которой вы можете столкнуться при исправлении конфликтующего доступа к памяти: иногда существует несколько способов исправить конфликт, который приводит к разным ответам, и не всегда очевидно, какой ответ является правильным. В этом примере, в зависимости от того, хотите ли вы исходную общую сумму или обновленную общую сумму, правильный ответ может быть либо 5, либо 320 долларов. Прежде чем вы сможете исправить конфликтующий доступ, вы должны определить, для чего он предназначен.

ЗАМЕТКА

Если вы написали параллельный или многопоточный код, конфликтный доступ к памяти может быть знакомой проблемой. Однако конфликтующий доступ, обсуждаемый здесь, может происходить в одном потоке и не включает в себя параллельный или многопоточный код.

Если у вас конфликтующий доступ к памяти из одного потока, Swift гарантирует, что вы получите ошибку во время компиляции или во время выполнения. Для многопоточного кода используйте Thread Sanitizer, чтобы помочь обнаружить конфликтующий доступ между потоками.

Характеристики доступа к памяти

Существует три характеристики доступа к памяти, которые необходимо учитывать в контексте конфликтующего доступа: является ли доступ чтением или записью, продолжительностью доступа и местоположением в памяти, к которому осуществляется доступ. В частности, конфликт возникает, если у вас есть два доступа, которые удовлетворяют всем следующим условиям:

  • По крайней мере, один из них - доступ для записи.
  • Они получают доступ к одному и тому же месту в памяти.
  • Их продолжительность перекрывается.

Разница между доступом для чтения и записи обычно очевидна: доступ для записи изменяет местоположение в памяти, а доступ для чтения - нет. Расположение в памяти относится к тому, к чему осуществляется доступ - например, к переменной, константе или свойству. Продолжительность доступа к памяти является мгновенной или долгосрочной.

Доступ является мгновенным, если другой код не может быть запущен после того, как этот доступ начался, но до его окончания. По своей природе два мгновенных доступа не могут происходить одновременно. Большая часть доступа к памяти происходит мгновенно. Например, все права на чтение и запись в приведенном ниже листинге кода являются мгновенными:

func oneMore(than number: Int) -> Int {

return number + 1

}



var myNumber = 1

myNumber = oneMore(than: myNumber)

print(myNumber)

// Prints "2"

Однако существует несколько способов доступа к памяти, называемых долговременными доступами, которые охватывают выполнение другого кода. Разница между мгновенным доступом и долгосрочным доступом заключается в том, что другой код может запускаться после запуска долгосрочного доступа, но до его завершения, что называется перекрытием . Долгосрочный доступ может перекрываться с другими долгосрочными и мгновенными доступами.

Перекрывающиеся доступы появляются в основном в коде, который использует входящие-выходные параметры в функциях и методах или методы изменения структуры. Конкретные виды кода Swift, которые используют долгосрочный доступ, обсуждаются в следующих разделах.

Конфликт доступа к входным и выходным параметрам

Функция имеет долгосрочный доступ на запись ко всем входным и выходным параметрам. Доступ на запись для входного-выходного параметра начинается после того, как все не входящие-выходные параметры были оценены, и длится в течение всей продолжительности вызова этой функции. Если имеется несколько входных и выходных параметров, доступ к записи начинается в том же порядке, в котором отображаются параметры.

Одним из последствий этого долговременного доступа для записи является то, что вы не можете получить доступ к исходной переменной, которая была передана как входная-выходная, даже если правила области действия и управление доступом в противном случае позволили бы это - любой доступ к оригиналу создает конфликт. Например:

var stepSize = 1



func increment(_ number: inout Int) {

number += stepSize

}



increment(&stepSize)

// Error: conflicting accesses to stepSize

В приведенном выше коде stepSizeэто глобальная переменная, и она обычно доступна изнутри increment(_:). Однако доступ на чтение stepSizeперекрывается с доступом на запись к number. Как показано на рисунке ниже, оба numberи stepSizeотносятся к одному и тому же месту в памяти. Доступы для чтения и записи относятся к одной и той же памяти, и они перекрываются, вызывая конфликт.

Одним из способов разрешения этого конфликта является создание явной копии stepSize:

// Make an explicit copy.

var copyOfStepSize = stepSize

increment(&copyOfStepSize)



// Update the original.

stepSize = copyOfStepSize

// stepSize is now 2

Когда вы делаете копию stepSizeперед вызовом increment(_:), становится ясно, что значение copyOfStepSizeувеличивается на текущий размер шага. Доступ на чтение заканчивается до начала доступа на запись, поэтому конфликта нет.

Другим следствием долгосрочного доступа на запись к входным и выходным параметрам является то, что передача одной переменной в качестве аргумента для нескольких входных и выходных параметров одной и той же функции вызывает конфликт. Например:

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и в playerTwoScoreкачестве аргументов не приводит к конфликту - есть два доступа к записи, которые перекрываются во времени, но они обращаются к разным местам в памяти. Напротив, передача playerOneScoreв качестве значения для обоих параметров приводит к конфликту, поскольку он пытается выполнить два доступа для записи в одно и то же место в памяти одновременно.

ЗАМЕТКА

Поскольку операторы являются функциями, они также могут иметь долгосрочный доступ к своим входным и выходным параметрам. Например, если бы balance(_:_:)была названа операторная функция <^>, запись привела бы к тому же конфликту, что и .playerOneScore <^> playerOneScorebalance(&playerOneScore, &playerOneScore)

Конфликт доступа к себе в методах

Мутантный метод в структуре имеет доступ selfдля записи на время вызова метода. Например, рассмотрим игру, в которой у каждого игрока есть количество здоровья, которое уменьшается при получении урона, и количество энергии, которое уменьшается при использовании специальных способностей.

struct Player {

var name: String

var health: Int

var energy: Int



static let maxHealth = 10

mutating func restoreHealth() {

health = Player.maxHealth

}

}

В приведенном restoreHealth()выше методе доступ для записи selfначинается с начала метода и продолжается до тех пор, пока метод не вернется. В этом случае внутри нет другого кода, restoreHealth()который мог бы иметь перекрывающийся доступ к свойствам Playerэкземпляра. Приведенный shareHealth(with:)ниже метод принимает другой Playerэкземпляр в качестве входного-выходного параметра, создавая возможность перекрывающихся доступов.

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

В приведенном выше примере вызов shareHealth(with:)метода для игрока Оскара, который делится здоровьем с игроком Марии, не вызывает конфликта. Существует доступ на запись oscarво время вызова метода, потому что oscarэто значение selfв мутирующем методе, и есть доступ на запись mariaдля той же самой продолжительности, потому что он mariaбыл передан как параметр in-out. Как показано на рисунке ниже, они имеют доступ к различным местам в памяти. Несмотря на то, что два доступа к записи перекрываются во времени, они не конфликтуют.

Однако, если вы передаете oscarв качестве аргумента shareHealth(with:), возникает конфликт:

oscar.shareHealth(with: &oscar)

// Error: conflicting accesses to oscar

Для метода мутации необходим доступ на запись в selfтечение срока действия метода, а для параметра in-out необходим доступ на запись в teammateтечение той же продолжительности. Внутри метода оба selfи teammateотносятся к одному и тому же месту в памяти - как показано на рисунке ниже. Два доступа для записи относятся к одной и той же памяти, и они перекрываются, вызывая конфликт.

 

Конфликт доступа к свойствам

Типы, такие как структуры, кортежи и перечисления, состоят из отдельных составляющих значений, таких как свойства структуры или элементы кортежа. Поскольку это типы значений, мутирование любой части значения приводит к мутации целого значения, что означает, что доступ для чтения или записи к одному из свойств требует доступа для чтения или записи ко всему значению. Например, перекрывающийся доступ на запись к элементам кортежа создает конфликт:

var playerInformation = (health: 10, energy: 20)

balance(&playerInformation.health, &playerInformation.energy)

// Error: conflicting access to properties of playerInformation

В приведенном выше примере вызов balance(_:_:)элементов кортежа приводит к конфликту, потому что есть перекрывающиеся доступы к записи playerInformation. Оба параметра передаются как входные playerInformation.healthи playerInformation.energyисходящие параметры, что означает balance(_:_:)необходимость доступа к ним для записи на время вызова функции. В обоих случаях доступ на запись к элементу кортежа требует доступа на запись ко всему кортежу. Это означает, что есть два доступа к записи playerInformationс продолжительностью, которая перекрывается, вызывая конфликт.

Приведенный ниже код показывает, что та же ошибка появляется при перекрывающихся доступах записи к свойствам структуры, которая хранится в глобальной переменной.

var holly = Player(name: "Holly", health: 10, energy: 10)

balance(&holly.health, &holly.energy) // Error

На практике большая часть доступа к свойствам конструкции может безопасно перекрываться. Например, если переменная hollyв приведенном выше примере заменена локальной переменной вместо глобальной переменной, компилятор может доказать, что перекрывающийся доступ к сохраненным свойствам структуры безопасен:

func someFunction() {

var oscar = Player(name: "Oscar", health: 10, energy: 10)

balance(&oscar.health, &oscar.energy) // OK

}

В приведенном выше примере здоровье и энергия Оскара передаются как два входных параметра в balance(_:_:). Компилятор может доказать, что безопасность памяти сохраняется, потому что два сохраненных свойства никак не взаимодействуют.

Ограничение на перекрывающийся доступ к свойствам структуры не всегда необходимо для сохранения безопасности памяти. Безопасность памяти является желаемой гарантией, но исключительный доступ является более строгим требованием, чем безопасность памяти, что означает, что некоторый код сохраняет безопасность памяти, даже если он нарушает монопольный доступ к памяти. Swift разрешает этот безопасный для памяти код, если компилятор может доказать, что неисключительный доступ к памяти все еще безопасен. В частности, это может доказать, что перекрывающийся доступ к свойствам структуры безопасен, если применяются следующие условия:

  • Вы получаете доступ только к сохраненным свойствам экземпляра, а не к вычисляемым свойствам или свойствам класса.
  • Структура - это значение локальной переменной, а не глобальной переменной.
  • Структура либо не захвачена никакими замыканиями, либо захвачена только неэкранирующими замыканиями.

Если компилятор не может доказать, что доступ безопасен, он не разрешает доступ.