9. Параллельный swap из двух потоков

Метод swap могут вызывать из разных потоков. В чём ошибка и как её исправить?

class UserSettings { private(set) var isFeatureEnabled: Bool private let lock = NSLock() init(_ isFeatureEnabled: Bool) { self.isFeatureEnabled = isFeatureEnabled } func swap(other: UserSettings) { lock.lock() let temp = other.isFeatureEnabled other.isFeatureEnabled = isFeatureEnabled isFeatureEnabled = temp lock.unlock() } } let a = UserSettings(isFeatureEnabled: true) let b = UserSettings(isFeatureEnabled: false) a.swap(other: b) print(a.isFeatureEnabled)

Дополнительные вопросы

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. Хуже того, при записи в один сегмент памяти программа может непредвиденно завершиться.

Answer_9_1
Data race

Захват обоих lock

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

lock.lock() other.lock.lock()

Поток 1 может успеть заблокировать себя, а поток 2 - себя. Тогда, будет взаимная блокировка потоков (deadlock). Вторая строчка не сможет быть вызвана на обоих потоках.

Answer_9_2
Deadlock

Порядок локов

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

ObjectIdentifier это уникальный идентификатор объекта, который еще и реализует протокол Comparable. Что нам это дает:

func swap(other: UserSettings) { let first: UserSettings let second: UserSettings if ObjectIdentifier(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.

Answer_9_3

Дополнительные вопросы

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.