其实iOS范畴很多文章都谈到了关于锁的文章,可是我为什么要在这儿从头写一篇文章呢?一是很多文章运用的观点依然是很老的观点,和我的测试成果不符合,二则是自己对这方面也比较陌生,所以就在最近从头整理一下自己对着方面的查询,整理一下这一块的知识点。

首要是一波比照,我运用了10^7次遍历,运用的开发言语是Swift,在iOS15.5体系版本的iPhone13真机上跑出的数据:

iOS中为什么会有这么多锁呢?

全体来说NSConditionLock的功能会略慢,可是其他的功能都类似,在这个量级的数据处理下,它们的表现都非常的接近。从图中能够看出功能最好的三个锁是os_unfair_lockpthread_mutex以及DispatchSemaphore,前两者是互斥锁,后者是信号量。

首要我想提出一个问题,那便是锁的意图是什么?

在聊锁的意图之前,那首要咱们来看一个概念,那便是**线程安全。**什么是线程安全?**我的定义是当多线程都需求操作某个同享数据时,并不会引起意料之外的状况,能确保该同享数据的正确性。**可是怎么去完成一个线程安全类呢?通用的办法便是在一些数据的操作上加锁。而锁的意图便是确保多线程操作同享数据时,能确保数据的准确性和可猜测性。

os_unfair_lock

我相信有很多人都阅读过ibireme关于锁的功能比照的闻名文章《不再安全的 OSSpinLock》,其间提到了OSSpinLock不再安全的理由,可是由此却引发一个问题,那便是OSSpinLock首要的运用场景是哪里呢?

咱们都知道在Objective-C中定义一个特点的时分,有时特点会被声明为atomic,这便是说这个特点的set操作和get操作是原子性的,那么怎么确保这些 操作的原子性呢?我想这个时分你现已猜到答案了,Apple运用的方案是OSSpinLock,这是一个自旋锁,可是这个锁有一个很严重的问题,那便是优先级回转问题会导致自旋锁产生死锁。

iOS 体系中保护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前履行,一个线程不会遭到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级回转问题,然后破坏了 spin lock。

具体来说,假如一个低优先级的线程取得锁并拜访同享资源,这时一个高优先级的线程也尝试取得这个锁,它会处于 spin lock 的忙等状况然后占用很多 CPU。此刻低优先级线程无法与高优先级线程争夺 CPU 时间,然后导致任务迟迟完不成、无法开释 lock。这并不仅仅理论上的问题,libobjc 现已遇到了很屡次这个问题了,所以苹果的工程师停用了 OSSpinLock。

苹果工程师 Greg Parker 提到,关于这个问题,一种解决方案是用 truly unbounded backoff 算法,这能避免 livelock 问题,但假如体系负载高时,它仍有可能将高优先级的线程堵塞数十秒之久;另一种方案是运用 handoff lock 算法,这也是 libobjc 现在正在运用的。锁的持有者会把线程 ID 保存到锁内部,锁的等候者会暂时贡献出它的优先级来避免优先级回转的问题。理论上这种模式会在比较复杂的多锁条件下产生问题,但实践上现在还全部都好。

而在iOS 10之后,Apple运用了os_unfair_lock来替代了OSSpinLock, 这是一个高功能的互斥锁,而不是自旋锁,假如是阻止两个线程能够一起拜访临界区,那么这个锁无疑能够很好的完结作业,包括上述的pthread_mutex_lock 以及信号量都能够,可是假如咱们需求锁具备某些特性,那么这个时分就需求其他多品种的锁了。

// os_unfair_lock的运用
var unfairLock = os_unfair_lock()
os_unfair_lock_lock(&unfairLock)
os_unfair_lock_unlock(&unfairLock)
// pthreadMutex的运用
var pthreadMutex = pthread_mutex_t()
pthread_mutex_lock(&pthreadMutex)
pthread_mutex_unlock(&pthreadMutex)

这儿再弥补说明一下,Apple运用在确保原子性时实际会调用到的办法如下:

static inline void reallySetProperty() {
		...
		if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
			//PropertyLocks是一个StripedMap<spinlock_t>类型的全局变量
	    //而StripedMap是一个用数组来完成的hashmap,key是指针,value是类型是spinlock_t目标
	    //而spinlock_t则是mutex_tt<LOCKDEBUG>的类,而mutex_tt类内部是由os_unfair_lock mLock来完成
	    //所以,PropertyLocks[slot]意图便是获取os_unfair_lock目标
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
		...
}

它经过地址从PropertyLocks数组中取出了spinlock_t锁,可是怎么运用地址作为数组下标呢?它运用了一个很奇妙的hash算法,来完成指针到数组下标的转化:

static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        // 这是一个哈希算法,能够将目标的地址转化为数组的下标
        // 使得数组元素在0~StripeCount之间
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
 }

当然这种办法也会偶然导致哈希抵触,两个不同的地址会导致获取到同一个Lock,这样会形成资源闲置,没有充分利用CPU的资源,可是不妨碍这个哈希算法全体上是高效的。

NSLock

既然现已有了功能比较高的互斥锁,那为什么还需求有其它这些杂七杂八的锁呢?比方说接下来咱们要提到的NSLock,这个锁也是一个互斥锁,而它是根据pthread_mutex_lock的封装,而在原有的基础上增加了一个特性那便是超时!没错这便是有其他各种锁的原因,给不同的锁不同的特性,以满意具体的开发场景,NSLock的API如下:

open class NSLock : NSObject, NSLocking {
    open func `try`() -> Bool
    open func lock(before limit: Date) -> Bool
    open var name: String?
}

在某些时分,超时这个特性对错常有用的,由于在一些可能产生死锁的场景中,运用NSLock能够让咱们有一个保险机制,即使产生了死锁,也能够在一定的时间之后走出加锁状况,康复到正常的程序处理逻辑。可是和以上的互斥锁相同,它都无法应对递归的状况,那运用什么来处理递归锁呢?NSRecursiveLock!

NSRecursiveLock

运用NSRecursiveLock能够使得该锁被同一线程屡次获取而不会导致线程死锁。可是每一次lock都对应一次unlock,这样unlock结束之后,锁才会开释。而望文生义,这品种型的锁被用于一个递归办法内部来防止线程被堵塞。

let rlock = NSRecursiveLock()
class RThread : Thread {
    override func main(){
        rlock.lock()
        print("Thread acquired lock")
        callMe()
        rlock.unlock()
        print("Exiting main")
    }
    func callMe(){
        rlock.lock()
        print("Thread acquired lock")
        rlock.unlock()
        print("Exiting callMe")
    }
}
var tr = RThread()
tr.start()
// 屡次请求锁,并不会导致崩溃,这便是递归锁的效果

NSConditionLock

条件锁满意NSLocking 协议,所以基本的NSLock类型锁的基本lock,unlock这种全局的锁办法它也是具备的,初度之外,它还具备自己的特性,通常状况下,当线程需求以某种特定的次序履行任务时,比方一个线程出产数据,而另一个线程消耗数据时,能够运用NSConditionLock(比方常见的出产者顾客模型)。接下来咱们来看一个实例:

let NODATA = 1
let GOTDATA = 2
let clock = NSConditionLock(condition: NODATA)
var shareInt = 0
class ProducerThread: Thread {
        override func main() {
            for _ in 0..<100 {
                clock.lock(whenCondition: NODATA)
                LockFile.ProducerThread.sleep(forTimeInterval: 0.5)
                sharedInt = sharedInt + 1
                NSLog("出产者:\(sharedInt)")
                clock.unlock(withCondition: GOTDATA)
            }
        }
}
    class ConsumerThread: Thread {
        override func main() {
            for _ in 0..<100 {
                clock.lock(whenCondition: GOTDATA)
                sharedInt = sharedInt - 1
                NSLog("顾客:\(sharedInt)")
                clock.unlock(withCondition: NODATA)
            }
        }
}
let pt = ProducerThread.init()
let ct = ConsumerThread.init()
pt.start()
ct.start()

当创立一个条件锁的时分,需求指定一个特定Int类型的值。而lock(whenCondition:) 办法当条件满意时会获取这个锁,或许条件和另一个线程在运用unlock(withCondition:) 开释锁时设置的值满意时,NSConditionLock目标就会获取锁履行后续的代码片段,可是当lock(whenCondition:) 办法没有获取锁的时分(条件没满意时),这个办法会堵塞线程的履行,直到取得锁停止。

NSCondition

NSCondition和前者是很容易混淆的,可是这个锁解决了什么问题呢?

当一个已取得锁的线程发现履行其作业所需的附加条件(它需求一些资源、另一个处于特定状况的目标等)暂时还没有得到满意时,它需求一种办法来暂停,而且一旦满意条件就持续作业的机制,可是怎么完成呢?能够经过连续的查看(忙等候)来完成,可是这样做的话,线程持有的锁会产生什么?咱们应该在等候时保留它们仍是开释它们?仍是在满意条件时再次取得它们?

NSCondition供给了一种简练的办法来供给了这种问题的解决方案,一旦一个线程被放在该Condition的等候列表中,它能够经过另一个线程Signal来唤醒。以下是具体的事例:

let cond = NSCondition.init()
var available = false
var sharedString = ""
class WriterThread: Thread {
        override func main() {
            for _ in 0..<100 {
                cond.lock()
                sharedString = ""
                available = true
                cond.signal()
                cond.unlock()
            }
        }
  }
  class PrinterThread: Thread {
        override func main() {
            for _ in 0..<100 {
                cond.lock()
                while (!available) {
                    cond.wait()
                }
                sharedString = ""
                available = false
                cond.unlock()
            }
        }
  }

当线程waits一个条件时,这个Condition目标会unlock当时锁而且堵塞线程。当Condition宣布信号时,体系会唤醒线程,然后这个Condition目标会在wait()或许wait(until:)回来之前,这个Condition目标会从头获取到它的锁,因而,从线程的角度来看,它似乎一直持有者锁(尽管半途它会失去锁)。

Dispatch Semaphore

最后咱们聊一聊信号量,简而言之,信号量是需求在不同的线程中进行确定和解锁时运用的锁。由于它的wait办法会堵塞当时线程,所以需求其他线程发来signal信号来唤醒它。

let semaphore = DispatchSemaphore.init(value: 0)
DispatchQueue.global(qos: .userInitiated).async {
	  // to do some thing
		semaphore.signal()
}
semaphore.wait() // will block thread

如上述例子相同,信号量通常用于确定一个线程,直到另外一个线程中事件的完结后宣布signal信号。从上述的测试图标,以及其他许多文章,信号量的速度是很快的。上述的出产者顾客模型也能够运用信号量来完成:

let semaphore = DispatchSemaphore.init(value: 0)
 DispatchQueue.global(qos: .userInitiated).async {
        while true {
            sleep(1)
            sharedInt = sharedInt + 1
            NSLog("出产了: \(sharedInt)")
            _ = semaphore.signal()
        }
	}
  DispatchQueue.global(qos: .userInitiated).async {
        while true {
            if sharedInt <= 0 {
                _ = semaphore.wait(timeout: .distantFuture)
            } else {
                sharedInt = sharedInt - 1
                NSLog("消耗了: \(sharedInt)")
            }
        }
    }

好了,简略说了一下我关于锁的整理,期望我们也能够从中学到一点东西吧~ 假如有什么问题,或许错误期望我们能够留言点拨。

  • 我正在参与技能社区创作者签约计划招募活动,点击链接报名投稿。

参考

1、《不再安全的OSSPinLock》

2、Apple Thread Programming Guide

3、Concurrency in Swift

4、thread safety in swift