Use Queue and Semaphore for Concurrency and Property Wrapper

Use queue and semaphore for concurrency and property wrapper?

FWIW, another option is reader-writer pattern with concurrent queue, where reads are done synchronously, but are allowed to run concurrently with respect to other reads, but writes are done asynchronously, but with a barrier (i.e. not concurrently with respect to any other reads or writes):

@propertyWrapper
class Atomic<Value> {
private var value: Value
private let queue = DispatchQueue(label: "com.domain.app.atomic", attributes: .concurrent)

var wrappedValue: Value {
get { queue.sync { value } }
set { queue.async(flags: .barrier) { self.value = newValue } }
}

init(wrappedValue value: Value) {
self.value = value
}
}

Yet another is NSLock:

@propertyWrapper
class Atomic<Value> {
private var value: Value
private var lock = NSLock()

var wrappedValue: Value {
get { lock.synchronized { value } }
set { lock.synchronized { value = newValue } }
}

init(wrappedValue value: Value) {
self.value = value
}
}

where

extension NSLocking {
func synchronized<T>(block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}

Or you can use unfair locks:

@propertyWrapper
class SynchronizedUnfairLock<Value> {
private var value: Value
private var lock = UnfairLock()

var wrappedValue: Value {
get { lock.synchronized { value } }
set { lock.synchronized { value = newValue } }
}

init(wrappedValue value: Value) {
self.value = value
}
}

Where

// One should not use `os_unfair_lock` directly in Swift (because Swift
// can move `struct` types), so we'll wrap it in a `UnsafeMutablePointer`.
// See https://github.com/apple/swift/blob/88b093e9d77d6201935a2c2fb13f27d961836777/stdlib/public/Darwin/Foundation/Publishers%2BLocking.swift#L18
// for stdlib example of this pattern.

final class UnfairLock: NSLocking {
private let unfairLock: UnsafeMutablePointer<os_unfair_lock> = {
let pointer = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1)
pointer.initialize(to: os_unfair_lock())
return pointer
}()

deinit {
unfairLock.deinitialize(count: 1)
unfairLock.deallocate()
}

func lock() {
os_unfair_lock_lock(unfairLock)
}

func tryLock() -> Bool {
os_unfair_lock_trylock(unfairLock)
}

func unlock() {
os_unfair_lock_unlock(unfairLock)
}
}

We should recognize that while these, and yours, offers atomicity, you have to be careful because, depending upon how you use it, it may not be thread-safe.

Consider this simple experiment, where we increment an integer a million times:

func threadSafetyExperiment() {
@Atomic var foo = 0

DispatchQueue.global().async {
DispatchQueue.concurrentPerform(iterations: 10_000_000) { _ in
foo += 1
}
print(foo)
}
}

You’d expect foo to be equal to 10,000,000, but it won’t be. That is because the whole interaction of “retrieve the value and increment it and save it” needs to be wrapped in a single synchronization mechanism.

But you can add an atomic increment method:

extension Atomic where Value: Numeric {
mutating func increment(by increment: Value) {
lock.synchronized { value += increment }
}
}

And then this works fine:

func threadSafetyExperiment() {
@Atomic var foo = 0

DispatchQueue.global().async {
DispatchQueue.concurrentPerform(iterations: iterations) { _ in
_foo.increment(by: 1)
}
print(foo)
}
}


How can they be properly tested and measured to see the difference between the two implementations and if they even work?

A few thoughts:

  • I’d suggest doing far more than 1,000 iterations. You want to do enough iterations that the results are measured in seconds, not milliseconds. I used ten million iterations in my example.

  • The unit testing framework is ideal at both testing for correctness as well as measuring performance using the measure method (which repeats the performance test 10 times for each unit test and the results will be captured by the unit test reports):

    test results

    So, create a project with a unit test target (or add a unit test target to existing project if you want) and then create unit tests, and execute them with command+u.

  • If you edit the scheme for your target, you can choose to randomize the order of your tests, to make sure the order in which they execute doesn’t affect the performance:

    Sample Image

    I would also make the test target use a release build to make sure you’re testing an optimized build.

  • Needless to say, while I am stress testing the locks by running 10m iterations, incrementing by one for each iteration, that is horribly inefficient. There simply is not enough work on each thread to justify the overhead of the thread handling. One would generally stride through the data set and do more iterations per thread, and reducing the number of synchronizations.

    The practical implication of this, is that in well designed parallel algorithm, where you are doing enough work to justify the multiple threads, you are reducing the number of synchronizations that are taking place. Thus, the minor variances in the different synchronization techniques are unobservable. If the synchronization mechanism is having an observable performance difference, this probably suggests a deeper problem in the parallelization algorithm. Focus on reducing synchronizations, not making synchronizations faster.

How to solve data race/ read and write problem with a help of semaphore/lock?

You asked:

Is it possible to solve read and write problem with a help of semaphore or lock?

Yes. The approach that you have supplied should accomplish that.

It is possible to make the solution having serial write and serial read but is it possible to have concurrent reads (giving the possibility to have concurrent reads at one time)?

That’s more complicated. Semaphores and locks lend themselves to simple synchronization that prohibits any concurrent access (including prohibiting concurrent reads).

The approach which allows concurrent reads is called the “reader-writer” pattern. But semaphores/locks do not naturally lend themselves to the reader-writer pattern without adding various state properties. We generally accomplish it with concurrent GCD queue, performing reads concurrently, but performing writes with a barrier (to prevent any concurrent operations):

class ThreadSafeContainerGCD<Value> {
private var value: Value
private let queue = DispatchQueue(label: ..., attributes: .concurrent)

func get() -> Value {
queue.sync { value }
}

func mutate(_ block: @escaping (inout Value) -> Void) {
queue.async(flags: .barrier) { block(&self.value) }
}

init(value: Value) {
self.value = value
}
}

A few observations:

  • Semaphores are relatively inefficient. In my benchmarks, a simple NSLock is much faster, and an unfair lock even more so.

  • The GCD reader-writer pattern, while more efficient than the semaphore pattern, is still not as quick as a simple lock approaches (even though the latter does not support concurrent reads). The GCD overhead outweighs the benefits achieved by concurrent reads and asynchronous writes.

But benchmark the various patterns in your use case and see which is best for you. See https://stackoverflow.com/a/58211849/1271826.

is GCD really Thread-Safe?

You said:

I have studied GCD and Thread-Safe. In apple document, GCD is Thread-Safe that means multiple thread can access. And I learned meaning of Thread-Safe that always give same result whenever multiple thread access to some object.

They are saying the same thing. A block of code is thread-safe only if it is safe to invoke it from different threads at the same time (and this thread safety is achieved by making sure that the critical portion of code cannot run on one thread at the same time as another thread).

But let us be clear: Apple is not saying that if you use GCD, that your code is automatically thread-safe. Yes, the dispatch queue objects, themselves, are thread-safe (i.e. you can safely dispatch to a queue from whatever thread you want), but that doesn’t mean that your own code is necessarily thread-safe. If one’s code is accessing the same memory from multiple threads concurrently, one must provide one’s own synchronization to prevent writes simultaneous with any other access.

In the Threading Programming Guide: Synchronization, which predates GCD, Apple outlines various mechanisms for synchronizing code. You can also use a GCD serial queue for synchronization. If you using a concurrent queue, one achieves thread safety if you use a “barrier” for write operations. See the latter part of this answer for a variety of ways to achieve thread safety.

But be clear, Apple is not introducing a different definition of “thread-safe”. As they say in that aforementioned guide:

When it comes to thread safety, a good design is the best protection you have. Avoiding shared resources and minimizing the interactions between your threads makes it less likely for those threads to interfere with each other. A completely interference-free design is not always possible, however. In cases where your threads must interact, you need to use synchronization tools to ensure that when they interact, they do so safely.

And in the Concurrency Programming Guide: Migrating Away from Threads: Eliminating Lock-Based Code, which was published when GCD was introduced, Apple says:

For threaded code, locks are one of the traditional ways to synchronize access to resources that are shared between threads. ... Instead of using a lock to protect a shared resource, you can instead create a queue to serialize the tasks that access that resource.

But they are not saying that you can just use GCD concurrent queues and automatically achieve thread-safety, but rather that with careful and proper use of GCD queues, one can achieve thread-safety without using locks.


By the way, Apple provides tools to help you diagnose whether your code is thread-safe, namely the Thread Sanitizer (TSAN). See Diagnosing Memory, Thread, and Crash Issues Early.

How to handle Race Condition Read/Write Problem in Swift?

You ask:

Why is Read not done with barrier ... ?.

In this reader-writer pattern, you don’t use barrier with “read” operations because reads are allowed to happen concurrently with respect to other “reads”, without impacting thread-safety. It’s the whole motivating idea behind reader-writer pattern, to allow concurrent reads.

So, you could use barrier with “reads” (it would still be thread-safe), but it would unnecessarily negatively impact performance if multiple “read” requests happened to be called at the same time. If two “read” operations can happen concurrently with respect to each other, why not let them? Don’t use barriers (reducing performance) unless you absolutely need to.

Bottom line, only “writes” need to happen with barrier (ensuring that they’re not done concurrently with respect to any “reads” or “writes”). But no barrier is needed (or desired) for “reads”.

[Why not] ... write with sync?

You could “write” with sync, but, again, why would you? It would only degrade performance. Let’s imagine that you had some reads that were not yet done and you dispatched a “write” with a barrier. The dispatch queue will ensure for us that a “write” dispatched with a barrier won’t happen concurrently with respect to any other “reads” or “writes”, so why should the code that dispatched that “write” sit there and wait for the “write” to finish?

Using sync for writes would only negatively impact performance, and offers no benefit. The question is not “why not write with sync?” but rather “why would you want to write with sync?” And the answer to that latter question is, you don’t want to wait unnecessarily. Sure, you have to wait for “reads”, but not “writes”.

You mention:

With the flip behavior, I am getting nil ...

Yep, so lets consider your hypothetical “read” operation with async:

public func object(forKey key: String) -> Any? {
var result: Any?

concurrentQueue.async {
result = self.dictionary[key]
}

return result
}

This effective says “set up a variable called result, dispatch task to retrieve it asynchronously, but don’t wait for the read to finish before returning whatever result currently contained (i.e., nil).”

You can see why reads must happen synchronously, because you obviously can’t return a value before you update the variable!


So, reworking your latter example, you read synchronously without barrier, but write asynchronously with barrier:

public func object(forKey key: String) -> Any? {
return concurrentQueue.sync {
self.dictionary[key]
}
}

public func set(_ value: Any?, forKey key: String) {
concurrentQueue.async(flags: .barrier) {
self.dictionary[key] = value
}
}

Note, because sync method in the “read” operation will return whatever the closure returns, you can simplify the code quite a bit, as shown above.

Or, personally, rather than object(forKey:) and set(_:forKey:), I’d just write my own subscript operator:

public subscript(key: String) -> Any? {
get {
concurrentQueue.sync {
dictionary[key]
}
}

set {
concurrentQueue.async(flags: .barrier) {
self.dictionary[key] = newValue
}
}
}

Then you can do things like:

store["Number"] = 10
print(store["Number"])
store["Number"] = 20
print(store["Number"])

Note, if you find this reader-writer pattern too complicated, note that you could just use a serial queue (which is like using a barrier for both “reads” and “writes”). You’d still probably do sync “reads” and async “writes”. That works, too. But in environments with high contention “reads”, it’s just a tad less efficient than the above reader-writer pattern.

What is the Swift equivalent to Objective-C's @synchronized ?

With the advent of Swift concurrency, we would use actors.

You can use tasks to break up your program into isolated, concurrent
pieces. Tasks are isolated from each other, which is what makes it
safe for them to run at the same time, but sometimes you need to share
some information between tasks. Actors let you safely share
information between concurrent code.

Like classes, actors are reference types, so the comparison of value
types and reference types in Classes Are Reference Types applies to
actors as well as classes. Unlike classes, actors allow only one task
to access their mutable state at a time, which makes it safe for code
in multiple tasks to interact with the same instance of an actor. For
example, here’s an actor that records temperatures:

actor TemperatureLogger {
let label: String
var measurements: [Int]
private(set) var max: Int

init(label: String, measurement: Int) {
self.label = label
self.measurements = [measurement]
self.max = measurement
}
}

You introduce an actor with the actor keyword, followed by its definition in a pair of braces. The TemperatureLogger actor has properties that other code outside the actor can access, and restricts the max property so only code inside the actor can update the maximum value.

For more information, see WWDC video Protect mutable state with Swift actors.


For the sake of completeness, the historical alternatives include:

  • GCD serial queue: This is a simple pre-concurrency approach to ensure that one one thread at a time will interact with the shared resource.

  • Reader-writer pattern with concurrent GCD queue: In reader-writer patterns, one uses a concurrent dispatch queue to perform synchronous, but concurrent, reads (but concurrent with other reads only, not writes) but perform writes asynchronously with a barrier (forcing writes to not be performed concurrently with anything else on that queue). This can offer a performance improvement over a simple GCD serial solution, but in practice, the advantage is modest and comes at the cost of additional complexity (e.g., you have to be careful about thread-explosion scenarios). IMHO, I tend to avoid this pattern, either sticking with the simplicity of the serial queue pattern, or, when the performance difference is critical, using a completely different pattern.

  • Locks: In my Swift tests, lock-based synchronization tends to be substantially faster than either of the GCD approaches. Locks come in a few flavors:

    • NSLock is a nice, relatively efficient lock mechanism.
    • In those cases where performance is of paramount concern, I use “unfair locks”, but you must be careful when using them from Swift (see https://stackoverflow.com/a/66525671/1271826).
    • For the sake of completeness, there is also the recursive lock. IMHO, I would favor simple NSLock over NSRecursiveLock. Recursive locks are subject to abuse and often indicate code smell.
    • You might see references to “spin locks”. Many years ago, they used to be employed where performance was of paramount concern, but they are now deprecated in favor of unfair locks.
  • Technically, one can use semaphores for synchronization, but it tends to be the slowest of all the alternatives.

I outline a few my benchmark results here.

In short, nowadays I use actors for contemporary codebases, GCD serial queues for simple scenarios non-async-await code, and locks in those rare cases where performance is essential.

And, needless to say, we often try to reduce the number of synchronizations altogether. If we can, we often use value types, where each thread gets its own copy. And where synchronization cannot be avoided, we try to minimize the number of those synchronizations where possible.



Related Topics



Leave a reply



Submit