I think the essence of the question is:
is the proper use of synchronize able to solve any thread-safe
problem?
Technically yes, but in practice it's advisable to learn and use other tools.
I'll answer without assuming previous knowledge.
Correct code is code that conforms to its specification. A good specification defines
- invariants constraining the state,
- preconditions and postconditions describing the effects of the operations.
Thread-safe code is code that remains correct when executed by multiple threads. Thus,
- No sequence of operations can violate the specification.1
- Invariants and conditions will hold during multithread execution without requiring additional synchronization by the client2.
The high level takeaway point is: thread-safe requires that the specification holds true during multithread execution. To actually code this, we have to do just one thing: regulate the access to mutable shared state3. And there are three ways to do it:
- Prevent the access.
- Make the state immutable.
- Synchronize the access.
The first two are simple. The third one requires preventing the following thread-safety problems:
- liveness
- deadlock: two threads block permanently waiting for each other to release a needed resource.
- livelock: a thread is busy working but it's unable to make any progress.
- starvation: a thread is perpetually denied access to resources it needs in order to make progress.
- safe publication: both the reference and the state of the published object must be made visible to other threads at the same time.
- race conditions A race condition is a defect where the output is dependent on the timing of uncontrollable events. In other words, a race condition happens when getting the right answer relies on lucky timing. Any compound operation can suffer a race condition, example: “check-then-act”, “put-if-absent”. An example problem would be
if (counter) counter--;
, and one of several solutions would be @synchronize(self){ if (counter) counter--;}
.
To solve these problems we use tools like @synchronize
, volatile, memory barriers, atomic operations, specific locks, queues, and synchronizers (semaphores, barriers).
And going back to the question:
is the proper use of @synchronize able to solve any thread-safe
problem?
Technically yes, because any tool mentioned above can be emulated with @synchronize
. But it would result in poor performance and increase the chance of liveness related problems. Instead, you need to use the appropriate tool for each situation. Example:
counter++; // wrong, compound operation (fetch,++,set)
@synchronize(self){ counter++; } // correct but slow, thread contention
OSAtomicIncrement32(&count); // correct and fast, lockless atomic hw op
In the case of the linked question you could indeed use @synchronize
, or a GCD read-write lock, or create a collection with lock stripping, or whatever the situation calls for. The right answer depend on the usage pattern. Any way you do it, you should document in your class what thread-safe guarantees are you offering.
1 That is, see the object on an invalid state or violate the pre/post conditions.
2 For example, if thread A iterates a collection X, and thread B removes an element, execution crashes. This is non thread-safe because the client will have to synchronize on the intrinsic lock of X (synchronize(X)
) to have exclusive access. However, if the iterator returns a copy of the collection, the collection becomes thread-safe.
3 Immutable shared state, or mutable non shared objects are always thread-safe.
@synchronized
correctly, it ensures thread-safety. As I read it, that answer is saying that if you misuse it (e.g. reference the wrong synchronization token), your code will not be thread-safe. But I think the same can be said of almost any synchronization technique, that if you use it incorrectly, your code will not be thread-safe. I think that lawicko's answer is otherwise quite good, but I think he over-states the case re@synchronized
. Regardless, there are better ways to ensure thread-safety. – Juicy