1. Достаточно ли lock у self, если меняется ещё и other?
2. Будет ли дедлок, если в каждом swap сначала брать lock у self, потом у other?
Темы
Компании
Ответ
Ответ
Если a.swap(other: b) будет вызван из одного потока, а b.swap(other: a) из другого, то может случиться следующее:
Поток 1 внутри swap дойдет до строчки isFeatureEnabled = temp (запись в a)
Поток 2 внутри swap дойдет до строчки other.isFeatureEnabled = isFeatureEnabled (тоже запись в a)
Таким образом у нас возникает data race. Хуже того, при записи в один сегмент памяти программа может непредвиденно завершиться.
Data race
Захват обоих lock
Решением может быть захват локов у обоих объектов, чтобы другой поток подождал полного завершения блока кода swap. Однако, если мы просто вызовем:
lock.lock()other.lock.lock()
Поток 1 может успеть заблокировать себя, а поток 2 - себя. Тогда, будет взаимная блокировка потоков (deadlock). Вторая строчка не сможет быть вызвана на обоих потоках.
Deadlock
Порядок локов
Поэтому локи нужно упорядочить. И поскольку мы имеем дело с классами, мы можем воспользоваться их свойством ObjectIdentifier.
ObjectIdentifier это уникальный идентификатор объекта, который еще и реализует протокол Comparable. Что нам это дает:
funcswap(other:UserSettings){let first:UserSettingslet second:UserSettingsifObjectIdentifier(self)<ObjectIdentifier(other){ first =self second = other
}else{ first = other
second =self} first.lock.lock() second.lock.lock()let temp = other.isFeatureEnabled
other.isFeatureEnabled = isFeatureEnabled
isFeatureEnabled = temp
second.lock.unlock() first.lock.unlock()}
Предположим, что ObjectIdentifier(a) меньше, чем ObjectIdentifier(b). Тогда поток 1, зайдя в функцию, сначала заблокирует lock a, а затем lock b. Для потока 2 картина будет иная. Он тоже сначала пойдёт к lock a, который уже заблокирован. Тогда он будет ждать завершения работы потока 1, не создавая deadlock.
Дополнительные вопросы
1. Нет. Lock у self не защищает поле у other. Пока один поток держит только свой lock, второй может писать в тот же isFeatureEnabled через чужой swap. Это и есть data race.
2. Если в каждом swap сначала брать lock у self, потом у other, дедлок как раз появится: a.swap(b) возьмёт a, b.swap(a) возьмёт b, дальше каждый ждёт чужой lock. Поэтому локи упорядочивают через ObjectIdentifier, а не по self / other.
swap берёт lock только у self, а пишет в оба объекта. Второй поток может в это же время менять other. Два вызова a.swap(other: b) и b.swap(other: a) пересекаются по полям.
Нужно запирать оба lock и всегда в одном порядке, иначе потоки возьмут их навстречу. Если в swap передали тот же объект, второй lock брать не надо: NSLock не реентерабельный.
funcswap(other:UserSettings){if other ===self{return}let first =ObjectIdentifier(self)<ObjectIdentifier(other)?self: other
let second = first ===self? other :self first.lock.lock() second.lock.lock()defer{ second.lock.unlock() first.lock.unlock()}let temp = other.isFeatureEnabled
other.isFeatureEnabled = isFeatureEnabled
isFeatureEnabled = temp
}
Дополнительные вопросы
1. Нет. Пока заперт только self, другой поток может держать lock у other и читать его флаг, либо писать в self, взяв lock уже у себя. Меняются оба объекта, значит нужны оба lock.
2. Да. a берёт свой lock и ждёт lock b. b берёт свой lock и ждёт lock a. Оба стоят друг на друге. Один и тот же порядок захвата это снимает.