Безопасность памяти
Структурируйте свой код таким образом, чтобы избежать конфликтов при доступе к памяти.
По умолчанию 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, чтобы помочь обнаружить конфликтующий доступ между потоками.
Характеристики доступа к памяти
Есть три характеристики доступа к памяти, которые следует учитывать в контексте конфликтующего доступа: чтение или запись, продолжительность доступа и местоположение в памяти, к которому осуществляется доступ. В частности, конфликт возникает, если у вас есть два доступа, которые соответствуют всем следующим условиям:
- Доступы не являются одновременно чтением и не являются атомарными.
- Они обращаются к одному и тому же месту в памяти.
- Их продолжительности перекрываются.
Разница между чтением и записью обычно очевидна: запись изменяет местоположение в памяти, но чтение — нет. Местоположение в памяти относится к тому, что используется — например, переменной, константе или свойству. Продолжительность доступа к памяти может быть мгновенной или длительной.
Доступ является атомарным, если это вызов атомарной операции на Atomic или AtomicLazyReference, или если он использует только атомарные операции C; в противном случае он не атомарный. Список функций атомарных операций C см. в руководстве stdatomic(3).
Доступ является мгновенным, если другим кодом невозможно запустить после начала доступа и до его завершения. По своей природе два мгновенных доступа не могут происходить одновременно. Большая часть доступа к памяти является мгновенной. Например, все обращения к чтению и записи в приведенном ниже фрагменте кода являются мгновенными:
func oneMore(than number: Int) -> Int {
return number + 1
}
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// Prints "2" Однако существуют несколько способов доступа к памяти, называемые долгосрочными обращениями, которые охватывают выполнение другого кода. Разница между мгновенным доступом и долгосрочным доступом заключается в том, что другой код может выполняться после начала долгосрочного доступа, но до его завершения, что называется перекрытием. Долгосрочный доступ может перекрываться с другими долгосрочными доступами и мгновенными доступами.
Перекрывающиеся обращения в основном встречаются в коде, использующем параметры in-out в функциях и методах или мутирующие методы структуры. Конкретные типы Swift-кода, использующие долгосрочный доступ, обсуждаются в следующих разделах.
Конфликтующий доступ к параметрам in-out
Функция имеет долгосрочный доступ для записи ко всем своим параметрам in-out. Доступ для записи к параметру in-out начинается после оценки всех параметров, не являющихся in-out, и длится в течение всего времени вызова этой функции. Если есть несколько параметров in-out, доступ для записи начинается в том же порядке, что и параметры.
Одним из последствий этого долгосрочного доступа для записи является то, что вы не можете получить доступ к исходной переменной, которая была передана как in-out, даже если правила области видимости и контроль доступа позволяют это — любой доступ к исходной переменной создает конфликт. Например:
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(©OfStepSize) // Update the original. stepSize = copyOfStepSize // stepSize is now 2
Когда вы создаете копию stepSize перед вызовом increment(_:), становится ясно, что значение copyOfStepSize увеличивается на текущий шаг. Доступ для чтения заканчивается до начала доступа для записи, поэтому конфликта нет.
Другим следствием долгосрочного доступа к записи для параметров 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 Функция balance(_:_:) выше изменяет свои два параметра, чтобы разделить общую сумму поровну между ними. Вызов ее с playerOneScore и playerTwoScore в качестве аргументов не приводит к конфликту — существует два доступа для записи, которые перекрываются во времени, но они обращаются к различным местам в памяти. В отличие от этого, передача playerOneScore в качестве значения для обоих параметров приводит к конфликту, поскольку это попытка выполнить две записи в одно и то же место в памяти одновременно.
Примечание: Поскольку операторы являются функциями, они также могут иметь долгосрочный доступ к своим параметрам in-out. Например, если
balance(_:_:)была функцией оператора, названной<^>, записьplayerOneScore <^> playerOneScoreпривела бы к тому же конфликту, что иbalance(&playerOneScore, &playerOneScore).
Конфликтующий доступ к self в методах
Мутирующий метод структуры имеет доступ для записи к 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 в качестве параметра 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 В приведенном примере вызов метода 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 передаются как параметры in-out, что означает, что 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 позволяет такой код, безопасный с точки зрения памяти, если компилятор может доказать, что неэксклюзивный доступ к памяти по-прежнему безопасен. В частности, он может доказать, что перекрывающийся доступ к свойствам структуры безопасен, если выполняются следующие условия:
- Вы обращаетесь только к хранимым свойствам экземпляра, а не к вычисляемым свойствам или свойствам класса.
- Структура является значением локальной переменной, а не глобальной.
- Структура не захватывается никакими замыканиями или захватывается только невыполняемыми замыканиями.
Если компилятор не может доказать безопасность доступа, он не разрешает этот доступ.
This source file is part of the Swift.org open source project
Copyright © 2014 - 2025 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception