Углубитесь в многопоточность с помощью DispatchQueues и других инструментов. Решение некоторых распространенных проблем, связанных с многопоточностью.

Часть 1 | Часть 2 | Часть 3 | Часть 4

Условия гонки и гонка данных

Определения для обоих из Википедии

Состояние гонки возникает в программном обеспечении, когда правильная работа компьютерной программы зависит от последовательности или времени выполнения процесса или потоков программы.

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

Давайте посмотрим код для основного состояния гонки. Как видите, есть две отдельные DispatchQueues. Оба они изменяют userCount. В первой очереди проверяется «if userCount == 1000». Если да, он будет увеличиваться. В этот момент userCount равен 1000. Перед тем, как queue1 увеличивает userCount, другой поток (в данном случае это queue2) изменяет userCount на 0. Затем queue1 продолжается. Но userCount больше не 1000 и после увеличения становится «1». Мы ожидали 1001. Однако он печатает «1». Это называется Состояние гонки.

queue1
queue2
queue2: 0
queue1: 1

Отловить подобные ошибки не всегда просто. Но как только вы это получите, станет легче. Давайте продолжим и посмотрим еще один пример, с которым вы можете столкнуться в реальном мире. На этот раз тоже есть Гонка данных.

Предположим, вы разрабатываете новую функцию для приложения социальной сети, и с помощью этой новой функции пользователи смогут выбирать и загружать несколько изображений. Если вы выполняете эту операцию синхронно, а не асинхронно, теоретически это займет 10-кратное время вместо x-времени. Конечно, расчет является теоретическим, и предполагается, что пользователь выберет 10 изображений.

Я выполнил код трижды, и вот что он напечатал;

["8F329352-19F4-42E7-84AB-D6A42C4E5EE8"]
Total count: 1
["AABAC902-BA49-43C8-B799-A7CE7E94F6C3", "832ABC73-081E-498E-B738-988D20EE6EE8", "E8B2AAE9-5483-4392-A11D-5E4045C8B45C", "C7FE0127-830D-44FC-BDC3-F1458CE7B948", "6CE99FD3-375B-4C68-85CC-0FE427D14452", "778200EC-B048-4681-95F3-4E43E1EB82FA", "9D3BAD38-400D-4A78-9C56-36899F3E4BAF", "4E341FC3-4534-41C1-8456-A8AB782955CE"]
Total count: 8
["AA115891-C42C-4E56-9C3B-F0A5D0993FE3", "E36FBF48-9101-4E7E-993E-4670BE8A3574", "7F2BA746-50E8-4A0F-9DAE-416B2E1E7AF0", "E1ED70DB-34E8-4551-AA47-2CF507168308", "3D6BC030-C2B5-497B-83C2-9EE87BCAD5C2", "1C8320B0-6955-42E7-91D7-CA6256768C85"]
Total count: 6

А если запустить код, попробовать несколько раз, может даже получиться ошибка.

Что ж, что-то странное. Потому что мы загружаем 10 изображений, но возвращаем 1 id. Иногда 8, иногда 6 ... Вы можете получить все десять штук. Почему это меняется? В реальном приложении это может произойти из-за того, что сеть может выйти из строя, или сервер может не отвечать на некоторые изображения, или какой-то процесс займет слишком много времени, что может вызвать ошибку тайм-аута. Но на самом деле мы не занимаемся сетями. Мы моделируем это и знаем, что все наши запросы завершаются успешно. Итак, как это произошло? Вот что происходит, когда несколько задач или потоков пытаются изменить данные одновременно. В нашем случае у нас выполняется 10 одновременных задач. Когда все заканчивается, все они пытаются записать массив «imageIds», и возникает гонка данных. Все эти проблемы можно было бы предотвратить, если бы метод добавления был потокобезопасным.

Безопасность потоков

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

Вот определение потоковой безопасности.



Как мы можем сделать наш код потокобезопасным в Swift?

В стандартной библиотеке много опций, я исправлю проблему с некоторыми из них и постараюсь объяснить, что это значит. Мы собираемся использовать NSLock, DispatchQueue, Barrier и DispatchSemaphore.

NSLock

Это простой механизм, обеспечивающий безопасность потоков. Проще говоря, тот, кто первым получит блокировку, будет иметь право изменять данные. Остальным придется подождать, пока задача не будет завершена и замок не будет «разблокирован».

Вот документация, и слушайте, когда Apple о чем-то предупреждает.



Посмотрим на код: как решить эту проблему с помощью NSLock;

Когда вы получаете блокировку, вы должны ее разблокировать. В противном случае это может создать больше проблем, а потом решит.

В строке 17 Снимаем блокировку в операторе Defer. Отсрочка очень полезна в таких ситуациях. Это предотвращает случаи, которые могут произойти из-за забывания разблокировки.

DispatchQueue

Эту проблему также можно решить с помощью DispatchQueue. И поскольку вы с ним знакомы, возможно, вам будет проще.

Вот код, как решить эту проблему с помощью DispatchQueue;

DispatchQueue + Барьер

В предыдущем коде мы использовали последовательную очередь. Если вам нужно такое же поведение с параллельной очередью. Вам необходимо использовать Барьер. Просто создайте еще одну параллельную очередь и отправьте флаг .barrier при использовании async {…}

switch result {
case .success(let id):
    anotherQueue.async(flags: .barrier) {
        imageIds.append(id)
    }
case .failure(let error):
    print(error)
}

Отправка

Он похож на NSLock. Но с помощью семафора вы можете контролировать, сколько процессов могут получить доступ к общим данным. В строке 4, DispatchSemaphore (значение: 1) мы разрешили только один процесс. Он может быть разным для разных сценариев. Документация Apple для DispatchSemaphore.

Попробуйте удалить строку 15 (semaphore.wait ()) и добавить эту строку, как показано ниже. Затем выполните код. Вы увидите, что он будет вести себя так, как если бы процессы загрузки были синхронными.

for image in images {
    semaphore.wait()
    API.upload(image) { ...

Это просто создание Race Condition & Data Race и решение этих проблем без каких-либо сторонних решений. Проблемы многопоточности могут быть решены с помощью различных инструментов, таких как Async / Await, Coroutines, Promises, Futures, FRP и т. Д. Но понимание потоковой передачи и доступа к памяти очень важно. В следующей части я напишу о других проблемах многопоточности и о том, что Swift должен их решить. Любая обратная связь будет оценена. Пожалуйста, обращайтесь ко мне, если у вас есть вопросы.

Часть 1 | Часть 2 | Часть 3 | Часть 4