Многопоточное программирование в среде Cocoa, Mac OS, iOS

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

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

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

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

  • Несколько потоков может улучшить реакцию восприятия приложения.
  • Несколько потоков может улучшить производительность приложений в реальном времени на многоядерных системах.

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

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

Альтернативы потокам

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

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

В таблице 1-1 перечислены некоторые из альтернатив потокам. Эта таблица включает в себя как заменяющие технологии для потоков (такие, как объекты операций и GCD), так и альтернативы, которые направлены на эффективное использование единственного потока, который у вас уже есть.

Таблица 1-1 альтернативные потокам технологии.
ТехнологияОписание
Объекты операцийВведенный в Mac OS X v10.5, Операция объекта является оболочкой для задач, которые обычно выполняются на втором потоке. Эта оболочка скрывает аспекты управления потоками, позволяя Вам сосредоточиться на самой задаче. Как правило, эти объекты используют в сочетании с объектом операции очереди, который фактически управляет выполнением операцией объектов на одном или нескольких потоках.

Для получения дополнительной информации о том, как использовать операции объектов см. в разделе "Параллелизм".

Grand Central Dispatch (GCD)Введенный в Mac OS X 10.6, Grand Central Dispatch является еще одной альтернативой для потоков, позволяет сосредоточиться на задачах, которые необходимо выполнить, а не на управлении потоками. В GCD, можно определить задачуи, которую требуется выполнить, и добавить ее в рабочую очередь, которая занимается планированием вашей задачи на соответствующий поток. Рабочая очередь учитывает количество доступных ядер и потоковую нагрузку для выполнения ваших задач более эффективно, чем вы могли бы это сделать самостоятельно с помощью потоков.

Для получения информации о том, как использовать GCD и очереди см. в разделе "Параллелизм".

Уведомления времени простояДля задач, которые являются относительно короткими и имеют очень низкий приоритет, уведомления режима ожидания позволяют выполнять задачи в то время, когда ваше приложение не так занято. Cocoa обеспечивает поддержку уведомлений времени простоя с использованием объекта NSNotificationQueue. Чтобы запросить уведомление времени простоя, посылают уведомление объекту NSNotificationQueue по умолчанию используя вариант NSPostWhenIdle. Очередь задерживает доставку уведомлений объекту до освобождения цикла выполнения.
Асинхронные функцииСистема интерфейсов включает множество асинхронных функций, которые обеспечивают, для вас, автоматический параллелизм. Эти интерфейсы могут использовать системные демоны и процессы или создавать собственные потоки для выполнения своих задач и возвращать для вас результаты. (Фактически реализация не имеет никакого значения, потому что она отделена от кода). При разработке приложения, обратите внимание на функции, которые предлагают асинхронное поведение и рассмотрите вопрос об использовании их вместо эквивалентных синхронных функций пользовательского потока.
ТаймерыВы можете использовать таймер в основном потоке приложения для выполнения периодических задач, которые являются слишком тривиальными для использования потока, но все-же требуют обслуживания на регулярной основе.
Отдельные процессыНесмотря на более тяжелый, чем потоки, создание отдельного процесса может оказаться полезным в тех случаях, когда задача имеет лишь косвенное отношение к вашему приложению. Вы можете использовать процесс, если задача требует значительного объема памяти, или должна быть выполнена с использованием привилегий. Например, вы можете использовать 64-разрядный серверный процесс для вычисления большого набора данных, в то время как 32-разрядное приложение отобразит результат пользователю.

Поддержка потоков

Если у вас уже есть код, который использует потоки, Mac OS X и iOS обеспечивают несколько технологий для создания потоков в приложениях. Кроме того, обе системы также обеспечивают поддержку управлением и синхронизацией задач на этих потоках. Далее описаны некоторые ключевые технологии, которые необходимо знать при работе с потоками в Mac OS X и iOS.

Потоковые пакеты

Хотя основной механизм реализации для потоков это Mach потоки, вы редко (если вообще) будете работать с потоками на уровне Mach ядра. Вместо этого обычно используют более удобный POSIX API или однин из его производных. Реализация Mach не предусматривает основные черты всех потоков, однако включает модель выполнения и возможность планирования потоков, чтобы они не зависили друг от друга.

Таблица 1-2 технологии потоков

ТехнологияОписание
Cocoa потокиCocoa реализует потоки с помощью класса NSThread. Cocoa также предоставляет методы NSObject для размножения новых потоков и выполнения кода на уже запущенных потоках.
POSIX потокиPOSIX потоки обеспечивают C-интерфейс для создания потоков. Если вы не пишете приложение Cocoa, то это лучший выбор для создания потоков. Интерфейс POSIX относительно прост в использовании и предоставляет достаточную гибкость для настройки ваших потоков.
Многопроцессорные сервисыМногопроцессорные сервисы являются наследием C-интерфейса, используемого приложениями для перехода от старых версий Mac OS. Эта технология доступна только в Mac OS X и ее следует избегать в любой новой разработки. Вместо этого следует использовать класс NSThread или POSIX потоки.

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

Когда вы создаете новый поток, необходимо указать функцию в качестве точки входа (или метод в качестве точки входа в случае Cocoa потоков) для этого потока. Эта функция точки входа является кодом, который вы хотите запустить на потоке. Когда функция возвращается, или при явном прекращении потока, поток останавливается и будет утилизирован в системе. Поскольку потоки являются относительно дорогими для создания в плане памяти и времени, поэтому рекомендуется, чтобы вашы функции точки входа делали более-менее значительный объем работы или устанавливали цикл выполнения, для повторяющихся задач, которые должны быть выполнены.

Циклы выполнения (Run-Loops)

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

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

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

Инструменты синхронизации

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

Замки обеспечивают грубую форму силы для защиты кода, который может быть выполнен только одним потоком одновременно. Наиболее распространенный тип блокировки взаимного исключения блокировки, также известный как мьютекс. Когда поток пытается получить мьютекс, там, где в настоящее время находится еще один поток, он блокируется, пока блокировка не будет освобождена от других потоков. Несколько framework-ов системы обеспечивают поддержку для замков мьютекс, хотя все они основаны на той же базовой технологии. Кроме того, Cocoa содержит несколько вариантов блокировки мьютекса для поддержки различных типов поведения, таких как рекурсия.

Кроме замков, система обеспечивает поддержку для условий, которые обеспечивают надлежащую последовательность задач в рамках вашего приложения. Условие действует как привратник, блокируя определенный поток до приведения определенного состояния в значение истина. Когда это произойдет, поток освобождается и продолжает выполняться. И POSIX и Foundation framework оба оказывают прямую поддержку условий. (Если вы используете объекты операций, вы можете настроить зависимости между объектами операции, последовательностью выполнения задач, которые очень похожи на поведение предлагаемое условиями.)

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

Междупотоковые соединения

Несмотря на то, что хороший дизайн сводит к минимуму количество необходимых связей, в какой-то момент связь между потоками становится необходимой. (Работа потока заключается в работе вашего приложения, но если результаты этой работы никогда не используются, какая польза от этого?) Потоку, возможно, придется обрабатывать новые запросы или сообщать о своем прогрессе выполнения основному потоку приложения. В этих случаях вам нужен способ, чтобы получить информацию из одного потока в другой. К счастью, тот факт, что потоки распологаются в одном и том же пространстве процесса, означает, что у вас есть много возможностей для общения потоков между собой.

Есть много способов организации связи между потоками, каждый со своими преимуществами и недостатками. В таблице ниже перечислены наиболее распространенные механизмы связи, которые можно использовать в Mac OS X. (за исключением очереди сообщений и Cocoa распределенных объектов, эти технологии также доступны в iOS). Методы в этой таблице перечислены в порядке возрастания сложности.

МеханизмОписание
Прямой обмен сообщениямиCocoa-приложения поддерживают возможность выполнять селекторы непосредственно на других потоках. Эта возможность означает, что один поток посуществу может выполнить метод на любом другом потоке. Поскольку они выполняются в контексте целевого потока, сообщения, отправленные таким образом сериализуются автоматически на этот поток.
Глобальные переменные, разделяемой памяти и объектовДругой простой способ для передачи информации между двумя потоками заключается в использовании глобальных переменных, общих объектов, или общих блоков памяти. Несмотря на то, что общие переменные быстры и просты, они также являются более хрупкими, чем прямой обмен сообщениями. Общие переменные должны быть тщательно защищены замками или другими механизмами синхронизации для обеспечения корректности кода. Невыполнение этого требования может привести к гонке условий, повреждению данных или полному ... .
УсловияУсловия представляют собой инструмент синхронизации, который можно использовать, чтобы контролировать, когда поток выполняет определенную часть кода. Вы можете думать об условиях как о стороже на воротах, позволяя потоку работать только тогда, когда указанные условия выполнены.
Циклы выполнения источниковПользовательским циклом выполнения источников является тот, который вы настроили на получение конкретных сообщений приложения на поток. Потому как они управляются событиями, цикл выполнения источников автоматически погрузит поток спать, когда нечего делать, что повысит эффективность вашего потока.
Порты и сокетыСвязи на основе порта являются более сложным способом связи между двумя потоками, но также и очень надежной техникой. Более того, портам и сокетам может использоваться для связи с внешними объектами, такими как другие процессы и сервисы. Для повышения эффективности порты реализованы с использованием источников выполнение цикла, поэтому ваш поток спит, когда нет данных, ожидающих портами.
Очереди сообщенийНаследие многопроцессорных сервисов определяет порядок первым вошел, первым вышел (FIFO (first-in, first-out)) для очереди абстракции управления входящими и исходящими данными. Несмотря на то, что очереди сообщений просты и удобны, они не столь эффективны, как и некоторые другие методы коммуникаций.
Cocoa-распределенные объектыРаспределенные объекты - технология Cocoa, которая обеспечивает высокий уровень реализации на основе портов связи. Несмотря на возможность использования этой технологии для меж-потоковых связей, делать этого крайне не рекомендуется, потому что сумму накладных расходов она берет на себя. Распределенные объекты гораздо больше подходит для общения с другими процессами, где накладные расходы уже и так очень высоки.

Советы по проектированию

Избегайте создания потока явно

Написание кода создания потока вручную утомительно и, возможность ошибок весьма высоко вероятна, так-что вы должны избегать этого "счастья" при любой возможности. Mac OS X и iOS обеспечивают неявную поддержку параллелизма через другие API. Вместо того, чтобы создавать поток самому, подумайте об использовании асинхронных API, GCD или операции объектов, чтобы сделать работу. Эти технологии делают работу на потоках, за кулисами для вас и гарантированно сделают все правильно. Кроме того, такие технологии, как GCD и операции объектов, управляют потоками намного эффективнее, чем ваш собственный код, регулируя количество активных потоков на основе текущей нагрузки на систему. Для получения дополнительной информации о GCD и объектах операции см. в "Параллелизм".

Держите свои потоки по возможности занятыми

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

Избегайте общих структур данных

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

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

Потоки и пользовательский интерфейс

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

Есть несколько исключений, где это выгодно при выполнении графических операций с другими потоками. Например, QuickTime API включает в себя ряд операций, которые могут быть выполнены из вторичных потоков, в том числе открытие видеофайлов, рендеринг файлов, сжатие видеофайлов, а также импорт и экспорт изображений. Кроме того, в Carbon и Cocoa можно использовать вторичные потоки для создания и обработки изображений и выполнения других связанных с изображением расчетов. Использование вторичных потоков для этих операций может значительно увеличить производительность. Если вы не уверены в частоте графических операций, планируйте все это из основного потока.

Быть в курсе поведения потока на время выхода

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

Создание не отделенного потока (также известны как объединяемые) требует дополнительных усилий с вашей стороны. Поскольку большинство высоких потоковых технологий, не создает объединяемые потоки по умолчанию, вы можете использовать POSIX API для создания потоков. Кроме того, необходимо добавить код для основного потока приложения для соединения с не отделенными потоками, когда они, наконец, закончат работу.

Если вы пишете Cocoa приложения, вы можете также использовать метод делегата applicationShouldTerminate:, чтобы отложить прекращение приложения на более позднее время или отменить его совсем. При задержке завершения, приложения должны ждать, пока любые критические потоки завершили свои задачи, а затем вызвать метод replyToApplicationShouldTerminate:.

Обработка исключений

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

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

Примечание: В Cocoa, объект NSException является автономным объектом, который может быть передан из потока в поток как только он был пойман.

В некоторых случаях, обработчики исключений могут быть созданы автоматически. Например, @synchronized директивы в Objective-C содержат неявного обработчика исключений.

Завершайте Ваши потоки чисто

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

Потоко- безопасность в библиотеках

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

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

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

Если вы разрабатываете библиотеки Cocoa, вы можете зарегистрироваться в качестве наблюдателя на NSWillBecomeMultiThreadedNotification, если вы хотите получать уведомления, когда приложение становится многопоточным. Вы не должны рассчитывать на получение этого уведомления, так, как оно может быть направлено, прежде чем ваша библиотека будет инициализирована и использована.

 
 
homeЗаметили ошибкукарта сайта 
   Made on a Mac