Будучи разработчиком iOS в автомобильной промышленности, я трачу много времени на работу с данными в реальном времени. Потребность в эффективной обработке непрерывных потоков данных - это то, что сегодня важно для многих приложений. Чтобы не блокировать пользовательский интерфейс, вам, скорее всего, понадобится многопоточность.
С информацией, которая передается в режиме реального времени, очень интересно работать, потому что вы будете постоянно получать новые данные, которые можно использовать для обновления визуальных элементов. Это также самая сложная и самая неприятная вещь, которую вы можете сделать, потому что устройство iOS имеет определенные ограничения, когда дело касается оборудования. К счастью, Apple сделала многопоточность доступной через чрезвычайно простой в использовании интерфейс под названием GCD (Grand Central Dispatch). Возможно, вы знакомы с кодом, который выглядит примерно так:
Основная очередь - это место, где выполняется большая часть кода вашей программы, если вы не помещаете его явно в другую очередь. Это последовательная очередь, то есть она выбирает первый элемент в строке, выполняет код, ожидает его завершения и освобождает элемент, затем выбирает следующий элемент в строке и т. Д.
Многопоточность и параллелизм
Однако основная очередь - не единственная очередь, доступная через GCD. Существует ряд предопределенных очередей с разными приоритетами. Вы также можете создать свои собственные специализированные очереди, например:
Обратите внимание, что очередь, которую мы только что создали, имеет атрибут .concurrent
, что означает, что эта конкретная очередь не будет ждать завершения одного элемента перед выполнением следующего. Он просто поместит первый элемент в поток и запустит его, а затем перейдет к следующему элементу, независимо от того, завершился первый элемент или нет.
А теперь давайте поговорим о технических деталях ...
Допустим, вы работаете с потоком данных с частотой дискретизации ~ 20 Гц. Это означает, что у вас будет около 50 миллисекунд на анализ и интерпретацию данных, добавление их в структуру данных и указание вашим представлениям отображать их. Если ваше устройство iOS попытается сделать это в основном потоке, у него будет очень мало времени, чтобы проверить, пытается ли пользователь взаимодействовать с приложением, и ваше приложение перестанет отвечать. Здесь мы переходим к многопоточности.
Предположим, что мы используем очень простую структуру данных для хранения получаемых нами выборок данных - общий целочисленный массив. У нас может возникнуть соблазн создать очередь и использовать ее следующим образом:
Это сработало ??
Выглядит хорошо, правда? Теперь мы выполняем всю обработку данных в фоновом потоке, а основной поток используется только для обновления визуальных элементов. Однако это неизбежно приведет к сбою. Но почему? Ответ носит технический характер, но важно подумать об этом.
Поскольку наша очередь является параллельной, она выбрасывает рабочие элементы в потоки, которые будут выполняться параллельно. Мы также используем массив в качестве хранилища данных. Массив Swift - это тип структуры, что означает, что это тип значения. Когда вы пытаетесь добавить значение в такой массив, вы:
- Выделите новый массив и скопируйте значения из старого массива
- Добавить новые данные
- Напишите новую ссылку обратно в вашу переменную
- Система переходит к освобождению памяти, которая использовалась старым массивом.
Подумайте, что произойдет, если два потока получат один и тот же массив, скопированный им, они добавят свои собственные данные в копию, а затем они запишут новую ссылку на нашу переменную, либо одну перед другой, либо обе одновременно. В первом случае мы получим неверные данные, потому что данные из потока, который записал первым, будут отсутствовать в массиве, записанном потоком, который записывает последним. Второй случай приведет к сбою нашего приложения, потому что два потока не могут одновременно получить доступ для записи в выделенную память.
Имея это в виду, мы могли бы использовать довольно умную конструкцию, которая поставляется с классом DispatchQueue, а именно флаги. Теперь мы можем изменить наш код следующим образом:
Это может показаться устрашающим, но я объясню, что он делает.
Используя флаг .barrier всякий раз, когда мы добавляем элемент, который изменяет нашу структуру данных путем записи в него, мы сообщаем нашей очереди, что этот конкретный рабочий элемент будет нужно выполнить самостоятельно. Это означает, что очереди нужно будет дождаться завершения всех запущенных потоков, затем запустить этот элемент и дождаться его завершения, после чего она может снова начать выполнение кода параллельно.
Когда основному потоку требуется доступ к данным для обновления наших представлений, он должен пройти через очередь данных с синхронным вызовом. В противном случае существует риск того, что один из наших потоков записи в любой момент повредит данные, которые он читает.
Заканчивать…
Надеюсь, вы прошли через это и по пути получили новые знания. Возможно, будет полезно перечитать это еще раз через пару дней, чтобы дать себе возможность поразмышлять над этим.
Не стесняйтесь комментировать, если у вас есть вопросы, и подписывайтесь, чтобы получать уведомления о будущих статьях.
Чтобы узнать больше о разработке для iOS, прочтите мою предыдущую статью: