10

Apple provides some documentation about synchronizing variables and even order of execution. What I don't see is any documentation on CPU cache behavior. What guarantees and control does the Objective-C developer have to ensure cache coherence between threads?

Consider the following where a variable is set on a background thread but read on the main thread:

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);
  });
}

Should count be volatile in this case?

Update 1

The documentation in Inter-thread Communication guarantees that shared variables can be used for inter-thread communication.

Another simple way to communicate information between two threads is to use a global variable, shared object, or shared block of memory.

Does this imply volatile is not required in this case? This is conflicting with the documentation in Memory Barriers and Volatile Variables:

If the variable is visible from another thread however, such an optimization might prevent the other thread from noticing any changes to it. Applying the volatile keyword to a variable forces the compiler to load that variable from memory each time it is used.

So I still don't know whether volatile is required because the compiler could use register caching optimizations or if it's not required because the compiler somehow knows it's a "shared" something.

The documentation is not very clear about what a shared variable is or how the compiler knows about it. In the above example, is count a shared object? Let's say count is an int, then it's not an object. Is it a shared block of memory or does that only apply to __block declared variables? Maybe volatile is required for non-block, non-object, non-global, shared variables.

Update 2

To everyone thinking this is a question about synchronization, it's not. This is about CPU cache behavior on the iOS platform.

John K
  • 859
  • 1
  • 8
  • 16
  • This depends on the declaration of `count`. As mentioned in a comment on your other recent question, property syntax is sugar for a method call. The method call will fetch the `_count` ivar; if the property is declared `atomic`, this will happen atomically, with synchronized access. If the property is `nonatomic`, I don't know what guarantees there may be. Citation needed at the moment, until I find supporting documentation. – Itai Ferber Feb 20 '17 at 01:34
  • For a bit more info, check out the "Properties Are Atomic by Default" section of the [Encapsulating Data](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/EncapsulatingData/EncapsulatingData.html#//apple_ref/doc/uid/TP40011210-CH5-SW37) doc page. – Itai Ferber Feb 20 '17 at 01:37
  • 1
    **tl;dr:** Atomic properties guarantee atomic access across threads and CPU caches, but be careful because that doesn't necessarily imply thread safety. `self.count += 1`, for instance, is _not_ an atomic operation (`self.count += 1` === `self.count = self.count + 1`; anything can happen between the read and the write). – Itai Ferber Feb 20 '17 at 01:38
  • http://stackoverflow.com/questions/29864995/does-dispatch-async-and-similar-provide-memory-fences – EricS Feb 20 '17 at 02:23
  • @ItaiFerber But the concept of an atomic operation is different than the concept of caching. Atomic means a read and write cannot occur at the same time. This does not imply that the data is actually pushed out to main memory or that cache is invalidated across processors. – John K Feb 20 '17 at 02:31
  • @EricS The concept of a memory barrier is different than the concept of caching. A memory barrier helps prevent out of order execution. It doesn't help ensure reads and writes are coherent across processors. – John K Feb 20 '17 at 02:33
  • 1
    It's my understanding that atomic, as implemented in Cocoa, DOES ensure coherency across processors, but can't swear to it. – Duncan C Feb 20 '17 at 02:34
  • In [Properties are Atomic](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/EncapsulatingData/EncapsulatingData.html#//apple_ref/doc/uid/TP40011210-CH5-SW37) it does say "synthesized accessors ensure that a value is always fully retrieved by the getter method or fully set via the setter method". I guess I was looking for documentation that is a bit more explicit. – John K Feb 20 '17 at 02:38
  • @JohnK Sure, but there are different classes of atomic operations — some ensure that reads and writes cannot interleave on the same thread, while others ensure memory barriers and cache invalidations across processors. I would bet $5 (but not my life) on the fact that `atomic` would ensure the strictest level of atomicity, purging caches and everything; that is the only definition that is truly useful across all atomicity requirements. It should be possible to verify this by looking at assembly produced by the compiler. I'll do that if you don't beat me to it first. – Itai Ferber Feb 20 '17 at 02:42
  • IIRC atomic is insufficient for guaranteeing that a write from one thread is read in another read. It is sufficient for guaranteeing that no partial writes will be read from the second thread. This is an important difference. I believe memory barriers on Mac/iOS do guarantee cross-processor cache coherence. Otherwise that code would never work and there would be no way to ever guarantee that the read would be pushed to the other cache. See the responses from Apple here: https://lists.apple.com/archives/cocoa-dev/2012/Dec/msg00135.html – EricS Feb 20 '17 at 03:00
  • @EricS one way to guarantee the cache coherence might be to declare the variable as volatile. If atomic does guarantee cache coherence, then a follow-up is how to maintain this guarantee in a custom atomic getter/setter. – John K Feb 20 '17 at 03:30
  • 2
    BTW, `volatile` does not ensure that you will be reading an uncached value: https://stackoverflow.com/questions/558848/can-i-force-cache-coherency-on-a-multicore-x86-cpu, https://stackoverflow.com/questions/18695120/volatile-and-cache-behaviour, https://stackoverflow.com/questions/7872175/c-volatile-variables-and-cache-memory?rq=1 (It only guarantees the compiler will not optimize reads away.) – Itai Ferber Feb 20 '17 at 05:57
  • The old `OSAtomic*` operations provided by the kernel have been deprecated in favor of the [C11 atomic functions](http://en.cppreference.com/w/c/atomic) — you can use those to ensure cache consistency based on your [memory ordering needs](http://en.cppreference.com/w/c/atomic/memory_order). From my experience, `atomic` properties produce the same instructions that the C11 atomics do (since they use the same compiler intrinsics), and with the default memory order being `memory_order_seq_cst` (the strictest), as mentioned before, I think you get what you want. – Itai Ferber Feb 20 '17 at 06:08
  • But you can always write custom getters and setters to use C11 atomics if you want to be sure. – Itai Ferber Feb 20 '17 at 06:09
  • Example above will most likely lead to data race. Read/write props on the same thread, pass copies around to avoid data races. – pronebird Feb 20 '17 at 10:45
  • 1
    [Memory Barriers and Volatile Variables](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html#//apple_ref/doc/uid/10000057i-CH8-SW9) "If the variable is visible from another thread however, such an optimization might prevent the other thread from noticing any changes to it. Applying the volatile keyword to a variable forces the compiler to load that variable from memory each time it is used." Either this implies volatile fixes the caching problem or this is very misleading and incomplete documentation. – John K Feb 20 '17 at 16:38
  • 1
    I'n not good with Objective C at all. But looking at the question from an architecture point of view, if you guard both read operation (main thread) and write operation (background thread) with a lock or an atomic block, then it is guaranteed that main thread will see any updates from the background thread. Simply because, only one will access the count variable at a time, when the other one try to access it later coherence protocol will kick in and will pass the updated value. – Isuru H Feb 27 '17 at 21:17
  • @IsuruH Well, this is the point. I think this is an interesting discussion, but simply using GCD for concurrent accesses solves that problem. – Amin Negm-Awad Mar 01 '17 at 17:50

3 Answers3

1

I know you are probably asking about the general case of using variables across threads (in which case the rules about using volatile and locks are the same for ObjC as it is for normal C). However, for the example code you posted the rules are a little different. (I'll be skipping over and simplifying things and using Xcode to mean both Xcode and the compiler)

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);
  });
}

I'm going to assume self is an NSObject subclass something like this:

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

Objective C is a superset of C, and if you've ever done any reverse engineering of ObjC you'll know that ObjC code (sort of, not quite) gets converted into C code before it's compiled. All [self method:object] calls get converted to objc_msgSend(self, "method:", object) calls and self is a C struct with ivars and other runtime info in it.

This means this code doesn't do quite what you might expect.

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

Just accessing something isn't just accessing the variable but is instead doing self->something (which is why you need to get a weak reference to self when accessing an ivar in an Objective C block to avoid a retain cycle).

The second point is Objective C properties don't really exist. self.count gets turned into [self count] and self.count = 5 gets turned into [self setCount:5]. Objective C properties are just syntax sugar; convenience save you some typing and make things look a bit nicer.

If you've been using Objective C for more than a few years ago you'll remember when you had to add @synthesize propertyName = _ivarName to the @implementation for ObjC properties you declared in the header. (now Xcode does it automatically for you)

@synthesize was a trigger for Xcode to generate the setter and getter methods for you. (if you hadn't written @synthesize Xcode expected you to write the setter and getter yourself)

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

If you are worried about threading issues with self.count you are worried about 2 threads calling these methods at once (not directly accessing the same variable at once, as self.count is actually a method call not a variable).

The property definition in the header changes what code is generated (unless you implement the setter yourself).

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

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

@property (nonatomic, assign)
_count = count;

TLDR

If you care about threading and want to make sure you don't read the value half way through a write happening on another thread then change nonatomic to atomic (or get rid of nonatomic as atomic is the default). Which will result in code generated something like this.

@property (atomic, assign) NSInteger count;

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

This won't guarantee your code is thread safe, but (as long as you only access the property view it's setter and getter) should mean you avoid the possibility of reading the value during a write on another thread. More info about atomic and nonatmoic in the answers to this question.

Community
  • 1
  • 1
Kyle Howells
  • 3,008
  • 1
  • 25
  • 35
0

You should use lock or some other synchronise mechanism to protect the shared variable. According to the documentation it said:

Another simple way to communicate information between two threads is to use a global variable, shared object, or shared block of memory. Although shared variables are fast and simple, they are also more fragile than direct messaging. Shared variables must be carefully protected with locks or other synchronization mechanisms to ensure the correctness of your code. Failure to do so could lead to race conditions, corrupted data, or crashes.

In fact the best way to protect the counter variable is to use the Atomic Operation. You can read the article: https://www.mikeash.com/pyblog/friday-qa-2011-03-04-a-tour-of-osatomic.html

Dr Rob Lang
  • 6,659
  • 5
  • 40
  • 60
lee jason
  • 39
  • 2
  • Mike's blog is a good introduction to these concepts, but it's lacking a section on [os_unfair_lock](https://developer.apple.com/reference/os/os_unfair_lock), the structure you should probably use for atomicity here. – CodaFi Mar 06 '17 at 17:40
  • This question is not regarding atomicity. It's regarding CPU cache behavior on iOS. – John K Mar 07 '17 at 05:02
0

The simplest way, and the way that is least challenging to the developer's brain, is to perform tasks on a serial dispatch queue. A serial dispatch queue, like the main queue, is a tiny single threaded island in a multi-threaded world.

gnasher729
  • 51,477
  • 5
  • 75
  • 98