Синхронизация

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

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

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

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

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

Атомарные операции

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

OS X и iOS включают многочисленные операции для выполнения основных математических и логических операций на 32-разрядных и 64-разрядных значениях. Среди этих операций атомарные версии сравнивать-и-подкачивать, теста-и-набора и операций тестировать-и-очищать. Для списка поддерживаемых атомарных операций посмотрите /usr/include/libkern/OSAtomic.h заголовочный файл или видит atomic страница справочника.

Барьеры памяти и энергозависимые переменные

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

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

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

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

Блокировки

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

Таблица 4-1 перечисляет некоторые блокировки, обычно использующиеся программистами. OS X и iOS обеспечивают реализации для большинства этих типов блокировки, но не всех их. Для неподдерживаемых типов блокировки столбец описания объясняет причины, почему те блокировки не реализованы непосредственно на платформе.

Табличные 4-1  типы Блокировки

Блокировка

Описание

Взаимное исключение

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

Рекурсивная блокировка

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

Блокировка чтения-записи

Блокировка чтения-записи также упоминается как совместно используемая монопольная блокировка. Если защищенная структура данных часто читается и изменяется только иногда, этот тип блокировки обычно используется в операциях более широкого масштаба и может значительно улучшить производительность. Во время нормального функционирования многократные читатели могут получить доступ к структуре данных одновременно. Когда поток хочет записать в структуру, тем не менее, это блокирует, пока все читатели не выпускают блокировку, в которой точке это получает блокировку и может обновить структуру. В то время как поток записи ожидает блокировки, новый блок потоков читателя, пока не закончен поток записи. Система поддерживает блокировки чтения-записи с помощью потоков POSIX только. Для получения дополнительной информации о том, как использовать эти блокировки, посмотрите pthread страница справочника.

Распределенная блокировка

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

Спин-блокировка

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

Перепроверяемая блокировка

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

Для получения информации о том, как использовать блокировки, посмотрите Используя Блокировки.

Условия

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

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

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

Выполните селекторные подпрограммы

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

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

Затраты синхронизации и производительность

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

Таблица 4-2 перечисляет некоторые приблизительные затраты, связанные со взаимными исключениями и атомарными операциями в неоспоримом случае. Эти измерения представляли средние времена, принятые несколько тысяч выборок. Как с временами создания потока, хотя, взаимоисключающие времена сбора (даже в неоспоримом случае) могут варьироваться значительно в зависимости от загрузки процессора, скорости компьютера и суммы доступной системы и памяти программ.

Табличное 4-2  Взаимное исключение и атомарные операционные затраты

Элемент

Приблизительная стоимость

Примечания

Взаимоисключающее время сбора

Приблизительно 0,2 микросекунды

Это - время сбора блокировки в неоспоримом случае. Если блокировка сохранена другим потоком, время сбора может быть намного больше. Числа были определены путем анализа средних и средних значений, сгенерированных во время взаимоисключающего сбора на основанной на Intel iMac с 2 процессорами GHz Core Duo и 1 ГБ RAM рабочий OS X v10.5.

Атомарный сравнивать-и-подкачивать

Приблизительно 0,05 микросекунды

Это - время сравнивать-и-подкачивать в неоспоримом случае. Числа были определены путем анализа средних и средних значений для работы и были сгенерированы на основанной на Intel iMac с 2 процессорами GHz Core Duo и 1 ГБ RAM рабочий OS X v10.5.

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

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

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

Потокобезопасность и сигналы

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

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

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

Для получения дополнительной информации о сигналах и устанавливающий сигнальные обработчики, посмотрите signal и sigaction страницы справочника.

Подсказки для ориентированных на многопотоковое исполнение проектов

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

Избегите синхронизации в целом

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

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

Поймите пределы синхронизации

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

Знайте об угрозах кодировать правильность

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

NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
 
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[arrayLock unlock];
 
[anObject doSomething];

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

Существует проблема с предыдущим примером, все же. Что происходит, если Вы выпускаете блокировку, и другой поток входит и удаляет все объекты из массива, прежде чем Вы будете иметь возможность выполниться doSomething метод? В приложении без сборки «мусора» объект, который содержит Ваш код, мог быть выпущен, уехав anObject указывая на недопустимый адрес памяти. Для решения проблемы Вы могли бы решить просто перестроить свой существующий код и выпустить блокировку после Вашего вызова к doSomething, как показано здесь:

NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
 
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject doSomething];
[arrayLock unlock];

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

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

NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
 
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject retain];
[arrayLock unlock];
 
[anObject doSomething];
[anObject release];

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

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

Не упустите мертвые блокировки и динамические взаимоблокировки

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

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

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

Используйте энергозависимые переменные правильно

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

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

Используя атомарные операции

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

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

Таблица 4-3 перечисляет доступные атомарные математические и логические операции и соответствующие имена функций. Эти функции все объявляются в /usr/include/libkern/OSAtomic.h заголовочный файл, где можно также найти полный синтаксис. 64-разрядные версии этих функций доступны только в 64-разрядных процессах.

Таблица 4-3  Атомарная математика и логические операции

Работа

Имя функции

Описание

Добавить

OSAtomicAdd32

OSAtomicAdd32Barrier

OSAtomicAdd64

OSAtomicAdd64Barrier

Добавляют два целочисленных значения вместе и хранят результат в одной из указанных переменных.

Инкремент

OSAtomicIncrement32

OSAtomicIncrement32Barrier

OSAtomicIncrement64

OSAtomicIncrement64Barrier

Постепенно увеличивает указанное целочисленное значение 1.

Декремент

OSAtomicDecrement32

OSAtomicDecrement32Barrier

OSAtomicDecrement64

OSAtomicDecrement64Barrier

Постепенно уменьшает указанное целочисленное значение 1.

Логический OR

OSAtomicOr32

OSAtomicOr32Barrier

Выполняет логический OR между указанным 32-разрядным значением и 32-разрядной маской.

Логический AND

OSAtomicAnd32

OSAtomicAnd32Barrier

Выполняет логический AND между указанным 32-разрядным значением и 32-разрядной маской.

Логический XOR

OSAtomicXor32

OSAtomicXor32Barrier

Выполняет логический XOR между указанным 32-разрядным значением и 32-разрядной маской.

Сравните и подкачайте

OSAtomicCompareAndSwap32

OSAtomicCompareAndSwap32Barrier

OSAtomicCompareAndSwap64

OSAtomicCompareAndSwap64Barrier

OSAtomicCompareAndSwapPtr

OSAtomicCompareAndSwapPtrBarrier

OSAtomicCompareAndSwapInt

OSAtomicCompareAndSwapIntBarrier

OSAtomicCompareAndSwapLong

OSAtomicCompareAndSwapLongBarrier

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

Тест и набор

OSAtomicTestAndSet

OSAtomicTestAndSetBarrier

Тесты немного в указанной переменной, наборы, укусившие к 1 и возвращающие значение старого бита как булево значение. Биты тестируются согласно формуле (0x80 >> (n & 7)) из байта ((char*)address + (n >> 3)) где n разрядное число и address указатель на переменную. Эта формула эффективно разбивает переменную в 8-разрядные размерные блоки и упорядочивает биты в каждом блоке наоборот. Например, для тестирования бита самого низкоуровневого (укусил 0) 32-разрядного целого числа Вы фактически укажете 7 для разрядного числа; точно так же для тестирования самого высокого порядка укусило (укусил 32), Вы укажете 24 для разрядного числа.

Тест и ясный

OSAtomicTestAndClear

OSAtomicTestAndClearBarrier

Тесты немного в указанной переменной, наборы, укусившие к 0 и возвращающие значение старого бита как булево значение. Биты тестируются согласно формуле (0x80 >> (n & 7)) из байта ((char*)address + (n >> 3)) где n разрядное число и address указатель на переменную. Эта формула эффективно разбивает переменную в 8-разрядные размерные блоки и упорядочивает биты в каждом блоке наоборот. Например, для тестирования бита самого низкоуровневого (укусил 0) 32-разрядного целого числа Вы фактически укажете 7 для разрядного числа; точно так же для тестирования самого высокого порядка укусило (укусил 32), Вы укажете 24 для разрядного числа.

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

Перечисление 4-1  , Выполняющее атомарные операции

int32_t  theValue = 0;
OSAtomicTestAndSet(0, &theValue);
// theValue is now 128.
 
theValue = 0;
OSAtomicTestAndSet(7, &theValue);
// theValue is now 1.
 
theValue = 0;
OSAtomicTestAndSet(15, &theValue)
// theValue is now 256.
 
OSAtomicCompareAndSwap32(256, 512, &theValue);
// theValue is now 512.
 
OSAtomicCompareAndSwap32(256, 1024, &theValue);
// theValue is still 512.

Для получения информации об атомарных операциях посмотрите atomic страница справочника и /usr/include/libkern/OSAtomic.h заголовочный файл.

Используя блокировки

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

Используя взаимоисключающую блокировку POSIX

Взаимоисключающие блокировки POSIX чрезвычайно просты в использовании из любого приложения. Для создания взаимоисключающей блокировки Вы объявляете и инициализируете a pthread_mutex_t структура. Чтобы заблокировать и разблокировать взаимоисключающую блокировку, Вы используете pthread_mutex_lock и pthread_mutex_unlock функции. Перечисление 4-2 показывает абсолютный код, требуемый инициализировать и использовать взаимоисключающую блокировку потока POSIX. Когда Вы будете сделаны с блокировкой, просто вызовите pthread_mutex_destroy высвобождать структуры данных блокировки.

Перечисление 4-2  Используя взаимоисключающую блокировку

pthread_mutex_t mutex;
void MyInitFunction()
{
    pthread_mutex_init(&mutex, NULL);
}
 
void MyLockingFunction()
{
    pthread_mutex_lock(&mutex);
    // Do work.
    pthread_mutex_unlock(&mutex);
}

Используя класс NSLock

NSLock возразите реализует основное взаимное исключение для приложений Какао. Интерфейс для всех блокировок (включая NSLock) фактически определяется NSLocking протокол, определяющий lock и unlock методы. Вы используете эти методы, чтобы получить и выпустить блокировку так же, как Вы были бы любое взаимное исключение.

В дополнение к стандартному поведению при блокировании, NSLock класс добавляет tryLock и lockBeforeDate: методы. tryLock если блокировка недоступна, метод пытается получить блокировку, но не блокирует; вместо этого, метод просто возвращается NO. lockBeforeDate: метод пытается получить блокировку, но разблокирует поток (и возвраты NO) если блокировка не получена в пределе требуемого времени.

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

BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];
...
while (moreToDo) {
    /* Do another increment of calculation */
    /* until there’s no more to do. */
    if ([theLock tryLock]) {
        /* Update display used by all threads. */
        [theLock unlock];
    }
}

Используя @synchronized директиву

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

- (void)myMethod:(id)anObj
{
    @synchronized(anObj)
    {
        // Everything between the braces is protected by the @synchronized directive.
    }
}

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

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

Для получения дополнительной информации о @synchronized директива, посмотрите Язык программирования Objective C.

Используя другие блокировки какао

Следующие разделы описывают процесс для использования нескольких других типов блокировок Какао.

Используя объект NSRecursiveLock

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

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

NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
 
void MyRecursiveFunction(int value)
{
    [theLock lock];
    if (value != 0)
    {
        --value;
        MyRecursiveFunction(value);
    }
    [theLock unlock];
}
 
MyRecursiveFunction(5);

Используя объект NSConditionLock

NSConditionLock объект определяет взаимоисключающую блокировку, которая может быть заблокирована и разблокирована с определенными значениями. Вы не должны путать этот тип блокировки с условием (см. Условия). Поведение несколько подобно условиям, но реализовано очень по-другому.

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

Блокировка и разблокирование методов это NSConditionLock объекты отвечают на, может использоваться в любой комбинации. Например, можно соединить a lock сообщение с unlockWithCondition:, или a lockWhenCondition: сообщение с unlock. Конечно, эта последняя комбинация разблокировала блокировку, но не могла бы выпустить потоки, ожидающие на значении особого условия.

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

id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
 
while(true)
{
    [condLock lock];
    /* Add data to the queue. */
    [condLock unlockWithCondition:HAS_DATA];
}

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

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

while (true)
{
    [condLock lockWhenCondition:HAS_DATA];
    /* Remove data from the queue. */
    [condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)];
 
    // Process the data locally.
}

Используя объект NSDistributedLock

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

В отличие от других типов блокировки, NSDistributedLock не соответствует NSLocking протокол и таким образом не имеет a lock метод. A lock метод блокировал бы выполнение потока и потребовал бы, чтобы система опросила блокировку на предопределенном уровне. Вместо того, чтобы налагать этот штраф на Ваш код, NSDistributedLock обеспечивает a tryLock метод и позволяет Вам решить, опросить ли.

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

Как с другими типами блокировок, когда Вы сделаны с помощью NSDistributedLock объект, Вы выпускаете его путем вызова unlock метод.

Используя условия

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

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

Следующие разделы показывают Вам, как использовать условия в Вашем коде.

Используя класс NSCondition

NSCondition класс обеспечивает ту же семантику как условия POSIX, но обертывает и требуемую блокировку и структуры данных условия в отдельном объекте. Результатом является объект, который можно заблокировать как взаимное исключение и затем ожидать на подобном условие.

Перечисление 4-3 показывает фрагмент кода, демонстрирующий последовательность событий для ожидания на NSCondition объект. cocoaCondition переменная содержит NSCondition возразите и timeToDoWork переменная является целым числом, сразу постепенно увеличивающимся от другого потока до сигнализации условия.

Перечисление 4-3  Используя условие Какао

[cocoaCondition lock];
while (timeToDoWork <= 0)
    [cocoaCondition wait];
 
timeToDoWork--;
 
// Do real work here.
 
[cocoaCondition unlock];

Перечисление 4-4 показывает, что код раньше сигнализировал условие Какао и постепенно увеличивал предикатную переменную. Необходимо всегда блокировать условие прежде, чем сигнализировать его.

Перечисление 4-4  , Сигнализирующее условие Какао

[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];

Используя условия POSIX

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

Перечисление 4-5 показывает основную инициализацию и использование условия и предиката. После инициализации и условие и взаимоисключающая блокировка, поток ожидания вводит некоторое время цикл с помощью ready_to_go переменная как ее предикат. Только, когда предикат установлен, и условие, впоследствии сообщенное, приводит в порядок поток ожидания, будят и начинают выполнять его работу.

Перечисление 4-5  Используя условие POSIX

pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean     ready_to_go = true;
 
void MyCondInitFunction()
{
    pthread_mutex_init(&mutex);
    pthread_cond_init(&condition, NULL);
}
 
void MyWaitOnConditionFunction()
{
    // Lock the mutex.
    pthread_mutex_lock(&mutex);
 
    // If the predicate is already set, then the while loop is bypassed;
    // otherwise, the thread sleeps until the predicate is set.
    while(ready_to_go == false)
    {
        pthread_cond_wait(&condition, &mutex);
    }
 
    // Do work. (The mutex should stay locked.)
 
    // Reset the predicate and release the mutex.
    ready_to_go = false;
    pthread_mutex_unlock(&mutex);
}

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

Перечисление 4-6  , Сигнализирующее блокировку условия

void SignalThreadUsingCondition()
{
    // At this point, there should be work for the other thread to do.
    pthread_mutex_lock(&mutex);
    ready_to_go = true;
 
    // Signal the other thread to begin work.
    pthread_cond_signal(&condition);
 
    pthread_mutex_unlock(&mutex);
}