Ответ 1
Фундаментальная проблема заключается в интерпретации "каждый поток получает свою собственную копию".
Да, мы часто используем типы значений для обеспечения безопасности потока, предоставляя каждому потоку свою собственную копию объекта (например, массив). Но это не то же самое, что утверждать, что типы значений гарантируют, что каждый поток получит свою собственную копию.
В частности, используя замыкания, несколько потоков могут пытаться изменить один и тот же объект типа значения. Вот пример кода, который показывает некоторый не поточно-безопасный код, взаимодействующий с типом значения Swift Array
:
let queue = DispatchQueue.global()
var employees = ["Bill", "Bob", "Joe"]
queue.async {
let count = employees.count
for index in 0 ..< count {
print("\(employees[index])")
Thread.sleep(forTimeInterval: 1)
}
}
queue.async {
Thread.sleep(forTimeInterval: 0.5)
employees.remove(at: 0)
}
(Как правило, вы не добавляете вызовы в sleep
; я добавлял их только для манифестации состояний гонки, которые в противном случае трудно воспроизвести. Вам также не следует мутировать объект из нескольких потоков без такой синхронизации, но я делаю это для иллюстрации эта проблема.)
В этих async
вызовах вы по-прежнему ссылаетесь на тот же массив employees
определенный ранее. Итак, в этом конкретном примере мы увидим, что он выдает "Bill", он пропустит "Bob" (хотя это был "Bill", который был удален), он выведет "Joe" (теперь второй элемент), и затем произойдет сбой при попытке доступа к третьему элементу в массиве, в котором теперь осталось только два элемента.
Теперь все, что я проиллюстрировал выше, это то, что один тип значения может быть видоизменен одним потоком при использовании другим, что нарушает безопасность потока. На самом деле существует целый ряд более фундаментальных проблем, которые могут проявиться при написании кода, который не является потокобезопасным, но приведенный выше пример является лишь немного надуманным.
Но вы можете гарантировать, что этот отдельный поток получит свою собственную копию массива employees
, добавив "список захвата" к первому async
вызову, чтобы указать, что вы хотите работать с копией исходного массива employees
:
queue.async { [employees] in
...
}
Или вы автоматически получите это поведение, если передадите этот тип значения в качестве параметра другому методу:
doSomethingAsynchronous(with: employees) { result in
...
}
В любом из этих двух случаев вы будете наслаждаться семантикой значений и увидите копию (или копию при записи) исходного массива, хотя исходный массив мог быть видоизменен в другом месте.
Суть в том, что я хочу сказать только то, что типы значений не гарантируют, что каждый поток имеет свою собственную копию. Тип Array
не является (как и многие другие типы изменяемых значений) поточно-ориентированным. Но, как и все типы значений, Swift предлагает простые механизмы (некоторые из них полностью автоматические и прозрачные), которые предоставляют каждому потоку свою собственную копию, что значительно упрощает написание потокобезопасного кода.
Вот еще один пример с другим типом значения, который делает проблему более очевидной. Вот пример, где сбой при написании поточно-безопасного кода возвращает семантически неверный объект:
let queue = DispatchQueue.global()
struct Person {
var firstName: String
var lastName: String
}
var person = Person(firstName: "Rob", lastName: "Ryan")
queue.async {
Thread.sleep(forTimeInterval: 0.5)
print("1: \(person)")
}
queue.async {
person.firstName = "Rachel"
Thread.sleep(forTimeInterval: 1)
person.lastName = "Moore"
print("2: \(person)")
}
В этом примере первое печатное выражение скажет "Рэйчел Райан", что не является ни "Робом Райаном", ни "Рэйчел Мур". Короче говоря, мы проверяем нашу Person
пока она находится во внутренне противоречивом состоянии.
Но, опять же, мы можем использовать список захвата, чтобы наслаждаться семантикой значений:
queue.async { [person] in
Thread.sleep(forTimeInterval: 0.5)
print("1: \(person)")
}
И в этом случае он скажет "Роб Райан", не обращая внимания на тот факт, что первоначальный Person
может быть мутирован другим потоком. (Понятно, что настоящая проблема не решается только использованием семантики значений в первом async
вызове, но и синхронизацией второго async
вызова и/или использованием там же семантики значений.)