Objective-C поведение кэша процессора

Apple предоставляет некоторую документацию о синхронизации переменных и даже порядке выполнения. То, что я не вижу, - это документация о поведении кэша CPU. Какие гарантии и управление разработчику Objective-C необходимо обеспечить согласованность кеша между потоками?

Рассмотрим следующее, где переменная задана в фоновом потоке, но читается в основном потоке:

self.count = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
  self.count = 5;
  dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"%i", self.count);
  });
}

Должен ли считаться неустойчивым в этом случае?

Обновление 1

Документация в Inter-thread Communication гарантирует, что совместно используемые переменные могут использоваться для межпоточной связи.

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

В этом случае это означает, что волатильность не требуется? Это противоречит документации в Барьеры памяти и изменчивые переменные:

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

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

В документации не очень понятно, что такое общая переменная или как компилятор знает об этом. В приведенном выше примере подсчитывается общий объект? Пусть говорят, что count - это int, тогда это не объект. Является ли это разделяемым блоком памяти или это относится только к объявленным переменным __block? Возможно, volatile требуется для неблокированных, необъектных, неглобальных, общих переменных.

Обновление 2

Для всех, кто думает, что это вопрос о синхронизации, это не так. Это касается поведения кэша процессора на платформе iOS.

Ответ 1

Я знаю, что вы, вероятно, спрашиваете об общем случае использования переменных по потокам (в этом случае правила об использовании volatile и блокировок одинаковы для ObjC, как и для обычного C). Тем не менее, для приведенного вами кода кода правила немного отличаются. (Я буду пропускать и упрощать вещи и использовать Xcode для обозначения как Xcode, так и компилятора)

self.count = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
  self.count = 5;
  dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"%i", self.count);
  });
}

Я собираюсь предположить, что self является подклассом NSObject что-то вроде этого:

@interface MyClass : NSObject {
    NSInteger something;
}
@property (nonatomic, assign) NSInteger count;
@end

Цель C - это надмножество C, и если вы когда-либо делали обратную разработку ObjC, вы узнаете, что код ObjC (вроде, не совсем) преобразуется в код C перед его компиляцией. Все вызовы [self method:object] преобразуются в вызовы objc_msgSend(self, "method:", object), а self - это C-структура с ivars и другая информация о времени выполнения в ней.

Это означает, что этот код не делает то, что вы можете ожидать.

-(void)doThing{
   NSInteger results = something + self.count;
}

Просто доступ к something - это не просто доступ к переменной, но вместо этого выполняется self->something (поэтому вам нужно получить слабую ссылку на себя при доступе к ivar в блоке Objective C, чтобы избежать цикла сохранения).

Второй момент - объекты Objective C на самом деле не существуют. self.count превращается в [self count], а self.count = 5 превращается в [self setCount:5]. Объективные свойства C - это просто синтаксический сахар; удобство спасет вас от ввода текста и сделайте вещи немного лучше.

Если вы используете Objective C более нескольких лет назад, вы запомните, когда вам пришлось добавить @synthesize propertyName = _ivarName в @implementation для свойств ObjC, которые вы указали в заголовке. (теперь Xcode делает это автоматически для вас)

@synthesize был триггером для Xcode для генерации методов setter и getter для вас. (если вы не написали @synthesize Xcode, ожидали, что вы сами напишите установщик и получатель)

// Auto generated code you never see unless you reverse engineer the compiled binary
-(void)setCount:(NSInteger)count{
    _count = count;
}
-(NSInteger)count{
    return _count;
}

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

Определение свойства в заголовке изменяет код, сгенерированный (если вы сами не выполняете настройку).

@property (nonatomic, retain)
[_count release];
[count retain];
_count = count;

@property (nonatomic, copy)
[_count release];
_count = [count copy];

@property (nonatomic, assign)
_count = count;

TL;DR

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

@property (atomic, assign) NSInteger count;

// setter
@synchronized(self) {
    _count = count;
}

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

Ответ 2

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

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

На самом деле лучшим способом защиты переменной счетчика является использование Atomic Operation. Вы можете прочитать статью: https://www.mikeash.com/pyblog/friday-qa-2011-03-04-a-tour-of-osatomic.html

Ответ 3

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