Если массивы являются типами значений и, следовательно, скопированы, то как они не являются потокобезопасными?

Чтение this Я узнаю, что:

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

Затем меня отвели в этот ответ и его комментарий

и сказали:

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

& о каждой теме получает свою собственную копию, мне сказали

если один поток обновляет массив (предположительно, чтобы вы могли видеть, что редактирование из другой очереди), что просто не применяется

, который просто не применяется < - Почему бы и нет?

Я изначально думал, что все это происходит потому, что массив, то есть тип значения, попадает в класс, но, к моему удивлению, мне сказали НЕ верно! Поэтому я снова вернусь к Swift 101: D

Ответ 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 вызова и/или использованием там же семантики значений.)

Ответ 2

Поскольку Array - тип значения, вы гарантированно, что у него есть единственный владелец direct.

Проблема возникает из-за того, что происходит, когда массив имеет более одного непрямого владельца. Рассмотрим этот пример:

Class Foo {
    let array = [Int]()

    func fillIfArrayIsEmpty() {
        guard array.isEmpty else { return }
        array += [Int](1...10)
    }
}

let foo = Foo();

doSomethingOnThread1 {
    foo.fillIfArrayIsEmpty()
}

doSomethingOnThread2 {
    foo.fillIfArrayIsEmpty()
}

Array имеет единственного прямого владельца: экземпляр foo, в котором он содержится. Однако оба потока 1 и 2 имеют право собственности на foo и транзитно, Array внутри него. Это означает, что они могут оба мутировать его асинхронно, поэтому могут возникать условия гонки.

Вот пример того, что может произойти:

  • Запуск 1 начинается

  • array.isEmpty оценивает значение false, защита проходит, и выполнение будет продолжать передавать его

  • В потоке 1 используется время процессора, поэтому он начал работу с процессором. Тема 2 запланирована ОС

  • В настоящее время работает Thread 2

  • array.isEmpty оценивает значение false, защита проходит, и выполнение будет продолжать передавать его

    Выполняется
  • array += [Int](1...10). Array теперь равно [1, 2, 3, 4, 5, 6, 7, 8, 9]

  • Тема 2 завершена и освобождает процессор, поток 1 запланирован ОС

  • Тема 1 возобновляется там, где она была остановлена.

    Выполняется
  • array += [Int](1...10). Array теперь равно [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9]. Это не должно было случиться!

Ответ 3

У вас неправильное предположение. Вы думаете, что независимо от того, что вы делаете со структурами, копия всегда будет волшебным образом. Не правда. Если вы их скопируете, они будут скопированы так просто.

class someClass{ 
var anArray : Array = [1,2,3,4,5]

func copy{
var copiedArray = anArray // manipulating copiedArray & anArray at the same time will NEVER create a problem
} 

func myRead(_ index : Int){
print(anArray[index])
}

func myWrite(_ item : Int){
anArray.append(item)
}
}    

Однако внутри ваших функций чтения и записи вы получаете доступ к anArray - без копирования, поэтому условия гонки могут возникать, если одновременно выполняются функции myRead и myWrite. Вы должны решить (см. здесь) проблему с помощью очередей.