Vladimir Buikliskii

Vladimir Buikliskii 

iOS разработчик, автор ТГ канала @swiftynew

93subscribers

27posts

goals4
13 of 30 paid subscribers
Наберу 30 подписчиков и начну разбирать алгоритмы
1 of 3
$3.64 of $365 raised
Сбор на верстку и выпуск книги "100 вопросов для iOS-разработчика". Средства пойдут на профессиональную верстку книги и ее последующий выпуск.

Разбор Thread Sanitizer: Гонки данных в Swift

https://www.avanderlee.com/swift/thread-sanitizer-data-races/
Thread Sanitizer, также известный как TSan, — это инструмент на основе LLVM для проверки проблем многопоточности в вашем коде, написанном на языках Swift и C. Он был впервые представлен в Xcode 8 и может стать отличным инструментом для обнаружения менее заметных ошибок в вашем коде, таких как гонки данных.
В WeTransfer Thread Sanitizer помог нам решить нестабильные тесты и странные сбои, которые мы не могли точно связать с какой-либо причиной. Возможно, вы ранее не использовали TSan, так как может быть не совсем понятно, что делает этот инструмент и как он работает. Поэтому пришло время углубиться и объяснить, как улучшить ваш код с помощью Thread Sanitizer.
Что такое гонки данных?
Прежде чем мы погрузимся в анализатор, сначала нужно понять, что именно мы ищем. Мы собираемся исправить проблему, называемую гонкой данных.
Гонки данных возникают, когда к одной и той же области памяти осуществляется доступ из нескольких потоков без синхронизации, и по крайней мере одно из обращений — запись. Гонки данных могут приводить к следующим проблемам:
Непредсказуемое поведениеПовреждение памятиНестабильные тестыСтранные сбои
Поскольку гонка данных непредсказуема, она может возникать несистематически при тестировании вашего приложения. Возможно, у вас будет сбой при запуске, который не повторится при следующем запуске приложения. Если это так, вы, возможно, столкнулись с гонкой данных.
Примеры гонок данных в Swift
Поняв, что такое гонка данных, вы сможете лучше выявлять потенциальные проблемы в своем коде. Однако иногда бывает не так очевидно, что код может привести к гонке данных. Поэтому рассмотрим несколько примеров, чтобы показать, что такое гонка данных.
В следующем фрагменте кода два разных потока получают доступ к одному и тому же свойству типа String:
Пример гонки данных, обнаруженный Thread Sanitizer
Поскольку фоновый поток записывает значение в переменную name, у нас есть как минимум один доступ на запись. Поведение непредсказуемо, так как оно зависит от того, выполнится ли сначала оператор print или запись. Это пример гонки данных, что подтверждает Thread Sanitizer.
Гонка данных, вызванная ленивой переменной
Ленивая переменная откладывает инициализацию экземпляра до момента, когда она вызывается впервые. Это означает, что запись данных произойдет в тот момент, когда ленивая переменная будет впервые использована. Если два потока одновременно обращаются к этой ленивой переменной в первый раз, может возникнуть гонка данных:
Использование Thread Sanitizer для обнаружения гонок данных
Приведенные выше примеры показывают, что гонка данных может легко возникнуть. В небольших фрагментах кода вы можете заметить это самостоятельно, но по мере роста вашего проекта задача становится намного сложнее. Поэтому пришло время воспользоваться помощью Thread Sanitizer.
Вы также можете включить его для ваших тестовых схем, что может быть эффективным способом выявления ошибок, связанных с доступом к данным.
Как включить Thread Sanitizer
Вы можете включить Thread Sanitizer в настройках схемы:
То же самое можно сделать и для ваших тестовых схем, что позволяет эффективно выявлять гонки данных.
Как работает Thread Sanitizer?
После включения Thread Sanitizer ваше приложение будет заново собрано с нуля. Компилятор добавит проверки вокруг каждого обращения к памяти, чтобы определить, участвует ли определенный доступ в гонке. Пример кода выше будет выглядеть следующим образом после преобразования компилятором:
Метод
recordAndCheckWrite
будет сохранять временную метку для каждого обращения и каждого потока, используемого анализатором для обнаружения гонки данных.
Существуют ли ограничения для Thread Sanitizer?
Thread Sanitizer имеет несколько ограничений:
- Поддерживается только для 64-битных версий macOS и симуляторов iOS и tvOS.
- watchOS не поддерживается.
- Нельзя использовать TSan на устройстве.
Влияние на производительность
Как указано в документации Apple, использование TSan может привести к снижению производительности:
Запуск вашего кода с этими диагностическими инструментами также вызывает замедление работы приложения в 2–20 раз. Чтобы улучшить использование памяти вашим кодом, скомпилируйте его с оптимизацией    -O1.
Как решить проблему гонки данных?
После того как вы узнали, что такое гонка данных и как ее обнаружить, пришло время написать решение, чтобы она больше не возникала. Как мы уже узнали, ошибки, связанные с доступом к данным, происходят, когда несколько потоков обращаются к одной и той же области памяти без должной синхронизации. Возьмем пример выше, и мы могли бы написать решение, используя очереди диспетчера:
Используя очередь блокировок, мы синхронизировали доступ и обеспечили, чтобы только один поток одновременно получал доступ к переменной name. Это базовое решение нашей проблемы. Если вы хотите узнать больше о более эффективном решении с улучшенной производительностью, рекомендую прочитать мою статью о Concurrent в Swift, объясняющую многопоточность.
Использование акторов для решения гонок данных
Фреймворк многозадачности, представленный на WWDC 2021, включил акторы для синхронизации доступа к данным. Это лучшее решение для устранения ошибок, связанных с доступом к данным, и оно должно быть проще, чем использование очередей диспетчера.
Чтобы узнать больше об актерах в Swift, рекомендую прочитать мою статью Акторы в Swift: как использовать и предотвращать гонки данных. Пример выше можно переписать с использованием акторов, создав контроллер для name:
Актор NameController отвечает за любой доступ к свойству name. Наш метод updateName будет переписан следующим образом:
Как видно, нам нужно использовать ключевые слова async/await, так как доступ становится асинхронным. Подробнее об этом можно узнать в моей статье Async await в Swift с примерами кода.
Заключение
Вот и все! Глубокое погружение в гонки данных в Swift. Надеюсь, теперь вы сможете начать находить ошибки и исправлять сбои, которые долгое время присутствовали в вашем коде. Регулярное использование Thread Sanitizer поможет вам предотвратить незаметные ошибки.
Subscription levels3

Базовый уровень

$1.21 per month
Подписавшись на базовый уровень, вы получите:
1) Доступ к переводу популярных статей на тему iOS-разработки.

Стандартный уровень

$4.8 per month
Подписавшись на стандартный уровень, вы получите:
1) Доступ к переводу официальной документации Swift(Notion).
2) Доступ к переводу популярных статей на тему iOS-разработки.
3) Подробный список библиотек с инструкциями по их настройке для удобной работы с проектами(Notion -  https://www.notion.so/).
4) Вопросы и ответы из реальных собеседований.
5) Ранний доступ к онлайн-версии книги "100 вопросов для iOS-разработчика" до официального релиза.(Сечйас  доступен черновик)
6) Доступ к закрытому чату.

Продвинутый уровень(Менторство)

$61 per month
Подписавшись на продвинутый уровень, вы получите:
1. Все материалы и преимущества стандартного уровня.
2.  4 часа личных консультаций в месяц (онлайн-созвон или разбор кода/проектов).
Go up