Будучи разработчиком iOS в автомобильной промышленности, я трачу много времени на работу с данными в реальном времени. Потребность в эффективной обработке непрерывных потоков данных - это то, что сегодня важно для многих приложений. Чтобы не блокировать пользовательский интерфейс, вам, скорее всего, понадобится многопоточность.

С информацией, которая передается в режиме реального времени, очень интересно работать, потому что вы будете постоянно получать новые данные, которые можно использовать для обновления визуальных элементов. Это также самая сложная и самая неприятная вещь, которую вы можете сделать, потому что устройство iOS имеет определенные ограничения, когда дело касается оборудования. К счастью, Apple сделала многопоточность доступной через чрезвычайно простой в использовании интерфейс под названием GCD (Grand Central Dispatch). Возможно, вы знакомы с кодом, который выглядит примерно так:

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

Многопоточность и параллелизм

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

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

А теперь давайте поговорим о технических деталях ...

Допустим, вы работаете с потоком данных с частотой дискретизации ~ 20 Гц. Это означает, что у вас будет около 50 миллисекунд на анализ и интерпретацию данных, добавление их в структуру данных и указание вашим представлениям отображать их. Если ваше устройство iOS попытается сделать это в основном потоке, у него будет очень мало времени, чтобы проверить, пытается ли пользователь взаимодействовать с приложением, и ваше приложение перестанет отвечать. Здесь мы переходим к многопоточности.

Предположим, что мы используем очень простую структуру данных для хранения получаемых нами выборок данных - общий целочисленный массив. У нас может возникнуть соблазн создать очередь и использовать ее следующим образом:

Это сработало ??

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

Поскольку наша очередь является параллельной, она выбрасывает рабочие элементы в потоки, которые будут выполняться параллельно. Мы также используем массив в качестве хранилища данных. Массив Swift - это тип структуры, что означает, что это тип значения. Когда вы пытаетесь добавить значение в такой массив, вы:

  1. Выделите новый массив и скопируйте значения из старого массива
  2. Добавить новые данные
  3. Напишите новую ссылку обратно в вашу переменную
  4. Система переходит к освобождению памяти, которая использовалась старым массивом.

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

Имея это в виду, мы могли бы использовать довольно умную конструкцию, которая поставляется с классом DispatchQueue, а именно флаги. Теперь мы можем изменить наш код следующим образом:

Это может показаться устрашающим, но я объясню, что он делает.
Используя флаг .barrier всякий раз, когда мы добавляем элемент, который изменяет нашу структуру данных путем записи в него, мы сообщаем нашей очереди, что этот конкретный рабочий элемент будет нужно выполнить самостоятельно. Это означает, что очереди нужно будет дождаться завершения всех запущенных потоков, затем запустить этот элемент и дождаться его завершения, после чего она может снова начать выполнение кода параллельно.

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

Заканчивать…

Надеюсь, вы прошли через это и по пути получили новые знания. Возможно, будет полезно перечитать это еще раз через пару дней, чтобы дать себе возможность поразмышлять над этим.

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

Чтобы узнать больше о разработке для iOS, прочтите мою предыдущую статью: