<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0"
  xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
  xmlns:atom="http://www.w3.org/2005/Atom">

<channel>

<title>// by kei_sidorov: posts tagged многопоточность</title>
<link>https://sidorov.tech/tags/mnogopotochnost/</link>
<description>Личный блог для статеек</description>
<author>Кирилл Сидоров</author>
<language>en</language>
<generator>E2 (v3576; Aegea)</generator>

<itunes:owner>
<itunes:name>Кирилл Сидоров</itunes:name>
<itunes:email></itunes:email>
</itunes:owner>
<itunes:subtitle>Личный блог для статеек</itunes:subtitle>
<itunes:image href="" />
<itunes:explicit></itunes:explicit>

<item>
<title>Устройство многопоточности в iOS</title>
<guid isPermaLink="false">4</guid>
<link>https://sidorov.tech/all/ustroystvo-mnogopotochnosti-v-ios/</link>
<pubDate>Mon, 16 Nov 2020 11:23:29 +0000</pubDate>
<author>Кирилл Сидоров</author>
<comments>https://sidorov.tech/all/ustroystvo-mnogopotochnosti-v-ios/</comments>
<description>
&lt;p&gt;В 2000 году Apple выпустила открытую unix-like ОС &lt;a href="https://ru.wikipedia.org/wiki/Darwin"&gt;Darwin&lt;/a&gt;, которая уже в следующем году послужит базой для первой версии Mac OS X — 10.0,  которая, в свою очередь, в будущем станет прародителем всех операционных систем Apple, начиная от современных macOS и iOS, заканчивая watchOS в часах и audioOS в «умных» колонках.&lt;/p&gt;
&lt;p&gt;&lt;cut/&gt;&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;🎓&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Эта статья написана по мотивам сессии «&lt;a href="https://youtube.com/watch?v=GVXyrLB1tbk"&gt;Устройство многопоточности в iOS&lt;/a&gt;» проходившей в рамках третьего сезона &lt;a href="https://podlodka.io/crew"&gt;Podlodka iOS Crew&lt;/a&gt;, спикером которой был &lt;a href="https://twitter.com/looneyconey"&gt;Александр Андрюхин&lt;/a&gt;. Я позволили себе изменить последовательность повествования, а так же более полно раскрыть некоторые темы, а некоторые вовсе убрать.&lt;/p&gt;
&lt;p&gt;В любом случае, статья не будет заменой видео, а видео — статье. &lt;a href="https://podlodka.io/ioscrew"&gt;Залетайте на остаток сезона&lt;/a&gt; — впереди целая неделя живых докладов, посвященых скиллам, которые нужны, чтобы сделать из обычного приложения крутой продукт! Приятным бонусом будет доступ к видео с сессиями первой недели про многопоточность.&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;Darwin построен на &lt;a href="https://ru.wikipedia.org/wiki/XNU"&gt;XNU&lt;/a&gt; — гибридном ядре, включающим в себя микроядро &lt;a href="https://ru.wikipedia.org/wiki/Mach"&gt;Mach&lt;/a&gt; и некоторые части ОС семейства BSD. В контексте этой заметке нам важно что Darwin получил от BSD модель процессов unix и модель тредов &lt;a href="https://ru.wikipedia.org/wiki/POSIX"&gt;POSIX&lt;/a&gt;, а от Mach — слегка переосмысленное предствление процессов как задач.&lt;/p&gt;
&lt;p&gt;Для работы с потоками ОС разработчикам доступна С библиотека &lt;a href="https://ru.wikipedia.org/wiki/POSIX_Threads"&gt;pthread&lt;/a&gt; — первый слой абстракции в наших операционных системах. Несмотря на то, что использовать её в своём коде можно и по сей день, Apple никогда не рекомендовала использовать pthread напрямую. Уже с первых версий версий Mac OS, разработчикам была доступна абстракция Apple поверх phread — &lt;a href="https://developer.apple.com/documentation/foundation/thread"&gt;NSThread&lt;/a&gt;.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/Runloop@2x.png" width="765" height="333" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;NSThread — второй слой абстракции, которая нам заботливо предоставила Apple. Помимо более привычного Objective-C синтаксиса для создания и управления потоками, корпорация предоставила &lt;a href="https://developer.apple.com/documentation/foundation/runloop"&gt;RunLoop&lt;/a&gt; — цикл обслуживания задач и событий. RunLoop использует инструменты микроядра Mach (порты, XPC, ивенты и задачи) для управления потоком POSIX и может перевести поток в режим сна, если ему нечего делать и пробудить, когда появиться работа.&lt;/p&gt;
&lt;p&gt;Конечно, пользоваться NSThread можно и по сей день, но стоит помнить, что создание потока — дорогая операция, т.к. сам поток, мы должны запросить у самой ОС. Кроме того, синхронизация потоков и доступа к ресурсам несколько неудобна для повседневной разработки, поэтому разработчики Apple задумались о решении проблемы удобства работы с асинхронным кодом.&lt;/p&gt;
&lt;h2&gt;Grand Central Dispatch&lt;/h2&gt;
&lt;p&gt;С выходом iOS 4, Apple подняла разработчиков выше еще на один уровень абстракции и представила &lt;a href="https://developer.apple.com/documentation/DISPATCH"&gt;Grand Central Dispatch&lt;/a&gt; и вводит понятие очередей и задач для организации асинхронного кода. GCD — это высокоуровневый API, позволяющее создавать пользовательские очереди, управлять задачами в них, решать вопросы синхронизации и делать это максимально эффективно.&lt;/p&gt;
&lt;p&gt;Т.к. очереди — это всего лишь слой абстракции, под капотом они используют всё те же системные треды, но механизм их создания и использования оптимизирован. CGD имеет пул предсозданных потоков и распределяет задачи эффективно, максимально утилизируя процессор если это необходимо. Разработчикам более не нужно думать о самих потоках, их создании и управлении.&lt;/p&gt;
&lt;p&gt;Помимо создании новой очереди вручную, GCD предоставляет доступ к главной очереди, на которой работает UI и доступ к нескольким системным (глобальным) очередям.&lt;/p&gt;
&lt;p&gt;Очереди GCD бывают двух типов:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;serial — последовательные&lt;/li&gt;
&lt;li&gt;concurrent — параллельные&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/Concurent@2x.png" width="809" height="393" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Не сложно догадаться, что на serial очереди задачи будут выполняться последовательно, друг за другом, а на параллельной будут выполнятся одновременно. По умолчанию очередь создаётся с последовательными выполнением задач, а чтобы создать concurrent очередь, необходимо явно это указать&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;let queue = DispatchQueue(&amp;quot;com.company.name.app&amp;quot;, attributes: .concurrent)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Как уже было сказано, GCD предоставляет уже созданные, глобальные очереди, которые отличаются приоритетом:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;samp&gt;global(qos: .userInteractive)&lt;/samp&gt; — Для заданий, которые взаимодействуют с пользователем в данный момент и занимают очень мало времени.&lt;/li&gt;
&lt;li&gt;&lt;samp&gt;global(qos: .userInitiated)&lt;/samp&gt; — Для заданий, которые инициируются пользователем и требуют обратной связи.&lt;/li&gt;
&lt;li&gt;&lt;samp&gt;global(qos: .utility)&lt;/samp&gt; — Для заданий, которые требуют некоторого времени для выполнения и не требуют немедленной обратной связи.&lt;/li&gt;
&lt;li&gt;&lt;samp&gt;global(qos: .background)&lt;/samp&gt; — Для заданий, не связанных с визуализацией и не критичных ко времени исполнения.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⚠️ Все глобальные очереди — очереди с параллельным выполнением задач.&lt;/p&gt;
&lt;h2&gt;Постановка задачи в очередь&lt;/h2&gt;
&lt;p&gt;Задачи в любою очередь, параллельную или последовательную, могут быть поставлены синхронно и асинхронно. При асинхронной постановке задачи в очередь, код, следующий за постановкой задачи в очередь, продолжит выполняться.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/Async@2x.png" width="809" height="478" alt="" /&gt;
&lt;/div&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;...
DispatchQueue.global().async {
   processImage()
}
doRequest() // выполнется сразу, не дожидаясь processImage()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;А в случае с синхронной постановкой, код, следующей за ней, не продолжит своё выполнение, пока не будет выполнена поставленная в очередь задача.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;...
DispatchQueue.global().sync {
   processImage()
}
doRequest() // будет ждать processImage()&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;DispatchWorkItem&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://developer.apple.com/documentation/dispatch/dispatchworkitem"&gt;DispatchWorkItem&lt;/a&gt; — специальный класс GCD, более объектно-ориентированную альтернативу замыканию (блоку) для постановки задачи в очередь. В отличии от обычной постановки задачи в очередь, DispatchWorkItem имеет возможность:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;указать приоритет задачи&lt;/li&gt;
&lt;li&gt;получить уведомление о завершении задачи&lt;/li&gt;
&lt;li&gt;отменить задачу&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Важно понимать, что отмена задания работает до момента старта задачи, то есть пока она находиться в очереди. Если GCD начал исполнять код в блоке DispatchWorkItem отмена задания не приведет ни к какому результату, код продолжит своё выполнение.&lt;/p&gt;
&lt;/div&gt;&lt;h2&gt;NSOperation&lt;/h2&gt;
&lt;p&gt;GCD представляет удобную абстракцию для написания асинхронного кода, как с точки зрения идеи, так и с точки зрения синтаксиса. Но так было не всегда. В Objective-C (да и в первых версиях Swift) оперирование очередями и задачами было не таким удобным как в современном Swift да и вообще шло вразрез самому названию языка программирования. Apple необходимо было предоставить объектно ориентированную альтернативу GCD, и она это сделала представив &lt;a href="https://developer.apple.com/documentation/foundation/operation"&gt;NSOperation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;NSOperations — это по сути те же очереди, вместо DispatchQueue здесь OperationQueue, а вместо DispatchWorkItem — Operation. Но помимо ООПшного синтаксиса API Operations предоставляет два крутых перимущества:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Возможность указывать максимальное кол-во выполняемых одновременно задач в очереди.&lt;/li&gt;
&lt;li&gt;Указывать зависимые операции, выстраивая таким образом иерархию операций. В таком случае операция пойдет на выполнение только тогда, когда все операции, на которых она зависит, завершатся.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Последний пункт очень удобен для построения цепочки запросов, когда для выполнения одного запроса нужна информация от нескольких других.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;class CustomOperation: Operation {
    var outputValue: Int?

    var inputValue: Int {
        return dependencies
            .filter({ $0 is CustomOperation })
            .first as? CustomOperation
            .outputValue ?? 0
    }
    ...
}

let operation1 = CustomOperation()
operation1.start()

let operation2 = CustomOperation()
operation2.addDependency(operation1)
operation2.start()&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Ошибки и проблемы&lt;/h2&gt;
&lt;p&gt;Говоря об асинхронности и многопоточности, нельзя обойти тему основных основных ошибок, которые разрабочики могут совершить при написании кода, выполняющегося параллельно.&lt;/p&gt;
&lt;h2&gt;Race Condition или состояние гонки&lt;/h2&gt;
&lt;p&gt;Race Condition или состояние гонки — это ошибка проектирования многопоточных систем, при которой доступ к ресурсу не синхронизирован и результат выполнения может зависеть от последовательности выполнения кода. Определение сложно понять без примера, поэтому лучше разобраться опираясь на какой-то пример.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/Racecodition@2x.png" width="809" height="296" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;На иллюстрации изображено два потока, в которых происходит увеличение какого-то кода. Чтобы инкрементировать значение переменной, процессору нужно будет совершить три действия:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;считать значение переменной из общей памяти в регистр&lt;/li&gt;
&lt;li&gt;увеличить значение в регистре на 1&lt;/li&gt;
&lt;li&gt;записать значение из регистра обратно в общую память&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Как можно заметить на иллюстрации, считывание значения переменной вторым потом происходит до того, как первый поток успел записать увеличенное значение. В итоге теряется одно увеличение счетчика что может быть как причиной безобидного бага, так и серьезной проблемы приводящей к аварийному завершению.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;💡&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Самым серьёзным последствием Race Condition считается кейс ПО медицинсокого аппарата для лучевой терапии &lt;a href="https://ru.wikipedia.org/wiki/Therac-25"&gt;Therac-25&lt;/a&gt;. Состояние гонки приводило к неправильным значениям в переменой, используемой для определения режима работы лучевого механизма.&lt;/p&gt;
&lt;/div&gt;&lt;h2&gt;Deadlock&lt;/h2&gt;
&lt;p&gt;Deadlock или взаимная блокировка — ошибка многопоточного ПО, при которой несколько потоков могут взаимно ожидать освобождения некоторого ресурса бесконечное время.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/Deadlock@2x.png" width="809" height="311" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Разберем на примере. Допустим задача, в потоке А блокирует доступ к некому ресурсу А, пусть это будет некий SettingsStorage. Задача А блокирует доступ к стораджу, читает оттуда некоторые значения, и что-то вычисляет. В это время стартует задача B и блокирует доступ к ресурсу B, пусть это будет база данных. Чтобы выполнить некоторые вычисления задаче В тоже потребовался доступ к SettingsStorage и задача начинает ждать, когда задача А его освободит. В это время, задаче А понадобился доступ к БД, но он уже заблокирован задачей В. Происходит взаимна блокировка: задача А ожидает БД, которая заблокирована задачей В, которая ожидает сторадж, заблокированный задачей А.&lt;/p&gt;
&lt;h2&gt;Инверсия приоритетов&lt;/h2&gt;
&lt;p&gt;Инверсия приоритетов — ошибка, приводящая к смене приоритетов у потоков, которой не предполагался разработчиком.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/PriorityInversion@2x.png" width="809" height="311" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Пусть у нас есть всего две задачи с разным приоритетом и всего 1 ресурс, пусть это снова будет БД. Первым в очередь помещается задача с низким приоритетом. Она выполняет свою работу и в момент времени Т1 ей понадобилась БД и она блокирует доступ к ней. Почти сразу же после этого, стартует высокоприоритетная задача и вытесняет низкоприоритетную. Все идет по плану до момента Т3, где высокоприоритетная задача пытается завладеть БД. Так как ресурс заблокирован, задача высокоприоритетная задача переводится в ожидание, а низкоприоритетная получает процессорное время. Временной промежуток T3-T4 называют ограниченной инверсией приоритетов. В этом промежутке наблюдается логическое несоответствие с правилами планирования — задача с более высоким приоритетом находится в ожидании в то время как низкоприоритетная задача выполняется.&lt;/p&gt;
&lt;div class="e2-text-video"&gt;
&lt;iframe src="https://www.youtube.com/embed/GVXyrLB1tbk" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
</description>
</item>


</channel>
</rss>