Recently, I sorted out the knowledge related to iOS lock and read many articles about iOS lock on the Internet, which are basically OSSpinlock originated from ibireme which is no longer safe. There are more or less the following questions about locks:

  1. What is a lock? Why is there a lock?
  2. What locks are there? What kinds can you divide them into?
  3. How does the lock perform?
  4. What is the relationship between these locks?
  5. Why is OSSpinLock unsafe? How to solve it?
  6. Relationships between locks?
  7. What is the specific use of the lock?

Let’s explain it one by one.

What is the lock

Lock is a synchronization tool to ensure thread safety. Before accessing data, each thread must acquire the acquire lock and release the release lock after accessing data. If the lock is already occupied, other threads trying to acquire the lock will wait or sleep until the lock is available again.

Why is there a lock? In multithreaded programming scenarios, multiple threads access the same shared data at the same time, which may lead to data race and data confusion. In this case, a synchronization mechanism is needed to ensure data security, and locking is the most common synchronization tool.

What are the lock

There are 11 types of locks that are commonly used in iOS development, but there are also other apis to achieve synchronization, such asserial queueSerial queue, etc., we sorted out a graph as follows:

According to the different characteristics of locks, we can get different classification, according to the waiting behavior of locks, can be roughly divided into mutex lock and spin lock.

The mutex

When a lock is attempted, if the lock is locked and has not yet been released, the thread goes to sleep waiting for the lock to be released. After the lock is released, the thread waiting for the resource is awakened to execute. The POSIX standard interface provides the Pthread_mutex mutex.

spinlocks

When a lock is attempted, if the lock is locked and has not yet been released, the thread waits for the lock to be released in a circular manner. Once the lock is released, the thread waiting for the resource will execute immediately. The POSIX standard interface provides pthread_SPIN spin locks.

The pthread_spin API was not found in the OpenSource ObjC4 and Swift Foundation source code, but Apple did not expose the pthread_spin API.

performance

Next, let’s do a performance test to see how the various locks behave. The full code is here.

//swift
/// Repeat the lock unlocking operation for 10W times
public func testLockPerformance(a) {
    let looppCount:Int = 100000
    var begin:TimeInterval = 0
    var end:TimeInterval = 0
    //spinlock
    begin = CFAbsoluteTimeGetCurrent()
    spinlock = OSSpinLock(a)for _ in 1.looppCount {
        OSSpinLockLock(&(self.spinlock!))
        OSSpinLockUnlock(&(self.spinlock!))
    }
    end = CFAbsoluteTimeGetCurrent(a)NSLog("spinlock:\((end-begin)*1000)")
    // There is a link to the full code.
}
Copy the code

Output log:Convert to a more intuitive bar chart:

After several attempts, the output is roughly similar, with pthread_mutex being the fastest, followed by pthread_RW_LOCK, and then OS_UNfair_lock.

Relationship between locks

What is the relationship between these locks? Through the implementation of these locks, it is found that most iOS locks are based onpthread_mutexPackage, incidentally sorted out a diagram, as follows:

At the bottom is the POSIX portable operating system interface. Provides mutex mutex, recursive, cond conditional, and RW read-write locks.

  1. pthread_mutex

Pthread_mutex is a POSIX mutex. OSSpinLock, NSLock, NSCondition, and NSConditionLock are encapsulated on this basis.

  1. pthread_mutex(recursive)

Pthread_mutex recursive version, set pthread_mutexattr_t mutex attribute type to PTHREAD_MUTEX_RECURSIVE, Synchronized (objc_sync) and NSRecursiveLock are encapsulated on this basis.

  1. pthread_cond

Condition variables, a mechanism for synchronization using global variables shared between threads. For safety, condition variables are always used with a mutex. NSCondition and NSConditionLock are also encapsulated on this basis.

  1. pthread_rwlock

Read/write lock: Enables multiple read/write operations, but reads and writes cannot be performed at the same time.

Let’s look at the use of each lock in detail.

pthread_mutex

POSIX interface mutex, common APIS:

Pthread_mutexattr_init (&attr) Optional, initialize the mutex attribute pthread_mutexattr_setType (&attr) Optional, set the mutex attribute type, Pthread_mutex_init (&mutex,&attr) initializes the mutex pthread_mutex_lock(&mutex) lock, Block pthread_mutex_trylock(&mutex) Attempts to lock the pthread_mutex_unlock(&mutex). 0 is returned successfully. Pthread_mutex_destroy (&mutexCopy the code

It’s easy to use.

//swift
public func test_pthread_mutex(a) {
    NSLog("start")
    mutex = pthread_mutex_t()
    pthread_mutex_init(&(self.mutex!), nil)
    for i in 1.2 {
        DispatchQueue.global(qos: .default).async {
            pthread_mutex_lock(&(self.mutex!))
            sleep(2)
            NSLog("\(i):"+Thread.current.description);
            pthread_mutex_unlock(&(self.mutex!))}}//trylock
    DispatchQueue.global(qos: .default).async {
        let retCode = pthread_mutex_trylock(&(self.mutex!))
        if( retCode = = 0) {
            sleep(2)
            NSLog("3:"+Thread.current.description);
            pthread_mutex_unlock(&(self.mutex!))}NSLog("3-1:"+Thread.current.description);
    }
    NSLog("end")}Copy the code

Pthread_mutex_trylock does not block the current thread and does not return 0 if the lock fails to be acquired. And continue to execute; The latter blocks the current thread, fails to acquire the lock, and the current thread sleeps.

pthread_mutex recursive

Pthread_mutex supports recursive locks. When initializing pthread_MUtex, set the pthread_mutexattr_t mutex attribute to PTHREAD_MUTEX_RECURSIVE.

//swift
public func test_pthread_mutex_recursive(a) {
    NSLog("start")
    mutex = pthread_mutex_t()
    var attr = pthread_mutexattr_t()
    pthread_mutexattr_init(&attr)
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)
    pthread_mutex_init(&(self.mutex!), &attr)
    for i in 1.2 {
        DispatchQueue.global(qos: .default).async {
            pthread_mutex_lock(&(self.mutex!))
            sleep(2)
            NSLog("\(i):"+Thread.current.description);
            pthread_mutex_unlock(&(self.mutex!))}}NSLog("end")}Copy the code

pthread_rwlock

Pthread_rwlock The Pthread_rwlock is a standard POSIX interface that provides read/write locks and supports multiple reads and writes. However, the reads and writes are mutually exclusive. Common apis are as follows:

/ / / destroyed
public func pthread_rwlock_destroy(_: UnsafeMutablePointer<pthread_rwlock_t>) -> Int32
/ / / initialized
public func pthread_rwlock_init(_: UnsafeMutablePointer<pthread_rwlock_t>
, _: UnsafePointer<pthread_rwlockattr_t>?) -> Int32
/// get read lock
public func pthread_rwlock_rdlock(_: UnsafeMutablePointer<pthread_rwlock_t>) -> Int32
/// Try to obtain the read lock
public func pthread_rwlock_tryrdlock(_: UnsafeMutablePointer<pthread_rwlock_t>) -> Int32
/// Try to get the write lock
public func pthread_rwlock_trywrlock(_: UnsafeMutablePointer<pthread_rwlock_t>) -> Int32
/// get the write lock
public func pthread_rwlock_wrlock(_: UnsafeMutablePointer<pthread_rwlock_t>) -> Int32
/ / / unlock
public func pthread_rwlock_unlock(_: UnsafeMutablePointer<pthread_rwlock_t>) -> Int32
Copy the code

It’s also easy to use.

//swift 
public func test_pthread_rwlock(a) {
    NSLog("start")
    self.rwlock = pthread_rwlock_t()
    // Initialize the read/write lock
    pthread_rwlock_init(&(self.rwlock!), nil)
    DispatchQueue.global(qos: .default).async {
	     / / read
        pthread_rwlock_rdlock(&(self.rwlock!))
        sleep(3)
        NSLog("read1:"+Thread.current.description)
        pthread_rwlock_unlock(&(self.rwlock!))}DispatchQueue.global(qos: .default).async {
	     / / read
        pthread_rwlock_rdlock(&(self.rwlock!))
        sleep(3)
        NSLog("read2:"+Thread.current.description)
        pthread_rwlock_unlock(&(self.rwlock!))}DispatchQueue.global(qos: .default).async {
	     / / write
        pthread_rwlock_wrlock(&(self.rwlock!))
        sleep(3)
        NSLog("write1:"+Thread.current.description)
        pthread_rwlock_unlock(&(self.rwlock!))}DispatchQueue.global(qos: .default).async {
	     / / write
        pthread_rwlock_wrlock(&(self.rwlock!))
        sleep(3)
        NSLog("write2:"+Thread.current.description)
        pthread_rwlock_unlock(&(self.rwlock!))}DispatchQueue.global(qos: .default).async {
	     / / read
        pthread_rwlock_rdlock(&(self.rwlock!))
        sleep(3)
        NSLog("read3:"+Thread.current.description)
        pthread_rwlock_unlock(&(self.rwlock!))}NSLog("end")}Copy the code

semaphore

A DispatchSemaphore is a signal that holds counts to control multithreaded access to a resource. There is an easy to understand 🌰 about semaphores. A semaphore is similar to an empty parking lot. A vehicle sends a signal when it exits, an empty space +1, a vehicle enters and sends a wait when it enters, an empty space -1. When an empty space <=0, the vehicle cannot enter and the wait is blocked. Let’s take a look at common apis:

/// send signal, semaphore +1
public func signal(a) -> Int
/// Wait for signal, semaphore -1
public func wait(a)
// set the latest waiting time
public func wait(timeout: DispatchTime) -> DispatchTimeoutResult
public func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult
Copy the code

It’s also easy to use:

//swift
public func test_semaphore(a) {
    NSLog("start")
    // Initializes signal 2 to allow two threads to access simultaneously
    semaphore = DispatchSemaphore(value: 2)
    for i in 1.4 {
        DispatchQueue.global(qos: .default).async {
        	  // Wait for the signal
            self.semaphore?.wait()
            sleep(2)
            NSLog("\(i):"+Thread.current.description);
        	  // Release the signal
            self.semaphore?.signal()
        }
    }
    NSLog("end")}Copy the code

serial queue

To mention serial queue a little bit, we can also synchronize access to critical sections by placing them in serial queues. Let’s see how to use it:

//swift
/// Declare serialQueue lazy
private lazy var serialQueue:DispatchQueue = {
        return DispatchQueue(label: "queue1", qos: .default, attributes: DispatchQueue.Attributes.init(rawValue: 0), autoreleaseFrequency: .inherit, target: nil)
    }()


public func test_serial_queue(a) {
    NSLog("start")
    for i in 1.4 {
        DispatchQueue.global(qos: .default).async {
        	  // Critical section access is placed in serial queue
            self.serialQueue.async {
                sleep(2)
                NSLog("\(i):"+Thread.current.description); }}}NSLog("end")}Copy the code

The Next

  1. Common locks in iOS
  2. iOS OSSpinLock
  3. Underlying analysis of iOS Synchronized
  4. IOS NSLock underlying analysis
  5. IOS Atomic low-level analysis

Refer to the link

swift foundation opensource objc4