什么是锁?
在程序中,当多个任务(或线程)同时访问同一个资源时,比如多个操作同时修改一份数据,可能会导致数据不一致。这时候,我们需要“锁”来确保同一时间只有一个任务能够操作这个数据,避免“抢占”问题。简单来说,锁就是一种机制,它能帮助你控制多个任务按顺序来操作资源。
按照锁的功能来进行分类,iOS常见的锁:自旋、互斥、递归、条件。。。
一、自旋锁
自旋锁的意思就是当资源被占有时,自旋锁不会引起其他调用者休眠,而是让其他调用者自旋,不停的循环访问自旋锁导致调用者处于busy-wait(忙等状态),直到自旋锁的保持者释放锁。自旋锁是为了实现保护共享资源一种锁机制,在任何时刻只能有一个保持者,也就是说在任何时刻只能有一个可执行单元获得锁。也正是因为其他调用者会保持自旋状态,使得在锁的保持者释放锁时能够即刻获得锁,效率非常高。但我们说调用者时刻自旋也是消耗CPU资源的,所以如果自旋锁的使用者保持锁的时间比较短的话,使用自旋锁是非常合适的,因为在锁释放之后省去了唤醒调用者的时间。
1.OSSpinLock(已弃用)
OSSpinLock
是一种轻量级的锁。当一个线程获取不到锁时,它不会进入睡眠状态,而是一直循环检查锁是否可用,这叫“自旋”。- 缺点:
OSSpinLock
已经被弃用,因为它容易导致“优先级反转”(低优先级线程获取锁,高优先级线程等待锁释放,造成高优先级线程无法执行)。
2.os_unfair_lock
-
os_unfair_lock
是OSSpinLock
的替代品。它解决了优先级反转问题,当一个线程无法获取锁时,会立即休眠而不是自旋。 -
适用场景:用于短时间的锁定操作,轻量、快速。
var unfairLock = os_unfair_lock_s()
func safeMethod() {
os_unfair_lock_lock(&unfairLock)
// 执行共享资源的操作
os_unfair_lock_unlock(&unfairLock)
}
用GCD模拟多线程,看是否输出是否按顺序输出:
import Foundation
// 初始化不公平锁
var unfairLock = os_unfair_lock_s()
// 共享资源(例子:计数器)
var sharedCounter = 0
// 线程安全的方法,增量计数器
func safeIncrement() {
// 加锁,确保只有一个线程可以访问共享资源
os_unfair_lock_lock(&unfairLock)
// 临界区:操作共享资源
sharedCounter += 1
print("计数器增加: \(sharedCounter)") // 输出当前计数器的值
// 解锁,允许其他线程访问共享资源
os_unfair_lock_unlock(&unfairLock)
}
// 使用DispatchQueue模拟多线程
let queue = DispatchQueue.global()
// 测试线程安全的方法
for _ in 1...10 {
queue.async {
safeIncrement() // 多个线程同时操作计数器
}
}
输出:
二、互斥锁
互斥锁和自旋锁类似,都是为了解决对某项资源的互斥使用,并且在任意时刻最多只能有一个执行单元获得锁,与自旋锁不同的是,互斥锁在被持有的状态下,其他资源申请者只能进入休眠状态,当锁被释放后,CPU会唤醒资源申请者,然后获得锁并访问资源。
1.pthread_mutex_t
pthread_mutex_t
是 POSIX 线程库提供的底层互斥锁,能确保同一时刻只有一个线程访问共享资源。- 适用场景:高效多线程编程,适合对性能要求高的场景。
var mutex = pthread_mutex_t()
pthread_mutex_init(&mutex, nil)
func safeMethod() {
pthread_mutex_lock(&mutex)
// 执行共享资源的操作
pthread_mutex_unlock(&mutex)
}
同样的GCD模拟多线程,看是否输出是否按顺序输出:
import Foundation
//初始化互斥锁(Mutex)
var mutex = pthread_mutex_t()
pthread_mutex_init(&mutex, nil)
// 共享资源(例子:计数器)
var sharedCounter = 0
// 线程安全的方法,增量计数器
func safeIncrement() {
// 加锁,确保只有一个线程可以访问共享资源
pthread_mutex_lock(&mutex)
// 临界区,操作共享资源
sharedCounter += 1
print("计数器增加: \(sharedCounter)") // 输出当前计数器的值
// 解锁,允许其他线程访问共享资源
pthread_mutex_unlock(&mutex)
}
// 使用DispatchQueue模拟多线程
let queue = DispatchQueue.global()
// 测试线程安全的方法
for _ in 1...10 {
queue.async {
safeIncrement() // 多个线程同时操作计数器
}
}
输出:
2.NSLock
- NSLock 是 Cocoa 提供的更高级的互斥锁,它比 pthread_mutex_t 更易于使用。
let lock = NSLock()
func safeMethod() {
lock.lock()
// 执行代码
lock.unlock()
}
GCD模拟多线程:
import Foundation
//初始化互斥锁(NSLock)
var lock = NSLock()
// 共享资源(例子:计数器)
var sharedCounter = 0
// 线程安全的方法,增量计数器
func safeIncrement() {
// 加锁,确保只有一个线程可以访问共享资源
lock.lock()
// 临界区,操作共享资源
sharedCounter += 1
print("计数器增加: \(sharedCounter)") // 输出当前计数器的值
// 解锁,允许其他线程访问共享资源
lock.unlock()
}
// 使用DispatchQueue模拟多线程
let queue = DispatchQueue.global()
// 测试线程安全的方法
for _ in 1...10 {
queue.async {
safeIncrement() // 多个线程同时操作计数器
}
}
3.@synchronized(仅支持 Objective-C)
- 这是 Objective-C 中提供的自动锁机制,是OC的语法糖,Swift 中无法直接使用。它可以帮助你简化锁定的逻辑。
@synchronized(self) {
// 执行共享资源操作
}
三、递归锁
递归锁可以被同一线程多次请求,而不会引起死锁,即在多次被同一个线程进行加锁时,不会造成死锁。这主要是用在循环或递归操作中。
递归锁也是通过 pthread_mutex_lock
函数来实现,在函数内部会判断锁的类型,如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,等到递归完毕之后,所有锁都会释放。
1.NSRecursiveLock
NSRecursiveLock 是一种递归锁,允许同一个线程多次获取同一把锁而不会导致死锁。这是 NSLock 无法做到的。
- 适用于需要多次锁定同一资源的场景。
let recursiveLock = NSRecursiveLock()
func recursiveFunction(count: Int) {
recursiveLock.lock()
if count > 0 {
print("Count: \(count)")
recursiveFunction(count: count - 1)
}
recursiveLock.unlock()
}
2.pthread_mutex_t (递归锁)
- pthread_mutex_t 也可以被设置为递归模式,用法类似 NSRecursiveLock。
四、条件锁
条件是信号量的另一种类型,当某个条件为true时,它允许线程相互发信号。条件通常用于指示资源的可用性或确保任务以特定顺序执行。当线程测试条件时,除非该条件已经为真,否则它将阻塞。它保持阻塞状态,直到其他线程显式更改并发出条件信号为止。条件和互斥锁之间的区别在于,可以允许多个线程同时访问该条件。
1.pthread_cond_t
pthread_cond_t
是一种条件锁,常与pthread_mutex_t
搭配使用,允许线程在满足特定条件时进行等待或唤醒。- 适用场景:用于需要等待某个条件满足的多线程场景。
2.NSCondition
- NSCondition 是一个高级条件锁,可以让线程根据某些条件来等待或唤醒。
- NSCondition 的底层是通过条件变量(condition variable) pthread_cond_t 来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程,比如常见的生产者-消费者模式。
let condition = NSCondition()
var isReady = false
func producer() {
condition.lock()
isReady = true
condition.signal() // 唤醒等待中的线程
condition.unlock()
}
func consumer() {
condition.lock()
while !isReady {
condition.wait() // 等待条件满足
}
// 执行消费操作
condition.unlock()
}
模拟:
import Foundation
let condition = NSCondition()
var isReady = false
// 生产者方法
func producer() {
condition.lock()
print("生产者正在准备资源...")
isReady = true
print("资源准备完成,通知消费者")
condition.signal() // 唤醒等待的消费者线程
condition.unlock()
}
// 消费者方法
func consumer() {
condition.lock()
print("消费者等待资源...")
while !isReady {
condition.wait() // 等待条件满足
}
print("资源已准备好,开始消费资源")
// 执行消费操作
condition.unlock()
}
// 模拟并发:使用DispatchQueue进行生产者和消费者的交互
let queue = DispatchQueue.global()
// 消费者等待资源
queue.async {
consumer()
}
// 模拟生产者延迟生产资源
queue.asyncAfter(deadline: .now() + 2) {
producer()
}
3.NSConditionLock
- NSConditionLock 是 NSCondition 的一种变体,基于条件值进行锁定和解锁,适用于更复杂的线程同步场景。
五、信号量
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号。
其实本质上,它通过维护一个计数值来控制同时可以访问某一资源的线程数量。
1.dispatch_semaphore_t
dispatch_semaphore_t
是 GCD 提供的信号量机制,用于控制同时访问某一资源的线程数量。- 适用场景:适合控制并发任务的数量。
let semaphore = DispatchSemaphore(value: 1)
func safeMethod() {
semaphore.wait() // 请求资源
// 执行共享资源操作
semaphore.signal() // 释放资源
}
2.pthread_mutex_t(作为信号量使用)
- 可以通过将
pthread_mutex_t
和pthread_cond_t
配合使用,达到类似信号量的效果。
六、读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。
读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁。
1.pthread_rwlock_t
pthread_rwlock_t
是一种读写锁,它允许多个线程同时读取资源,但在写入时会排他性地锁定。- 适用场景:适合读多写少的场景。
var rwlock = pthread_rwlock_t()
pthread_rwlock_init(&rwlock, nil)
func readResource() {
pthread_rwlock_rdlock(&rwlock) // 加读锁
// 读取资源
pthread_rwlock_unlock(&rwlock) // 解锁
}
func writeResource() {
pthread_rwlock_wrlock(&rwlock) // 加写锁
// 写入资源
pthread_rwlock_unlock(&rwlock) // 解锁
}
七、栅栏
栅栏函数在GCD中常用来控制线程同步,在队列中它总是等栅栏之前的任务执行完,然后执行栅栏自己的任务,执行完自己的任务后,再继续执行栅栏后的任务。常用函数有同步栅栏函数(dispatch_barrier_sync)和异步栅栏函数(dispatch_barrier_async)。
import Foundation
// 创建一个并发队列
let queue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
// 异步执行任务1
queue.async {
print("Task 1")
}
// 异步执行任务2
queue.async {
print("Task 2")
}
// 使用 barrier 标志的任务,确保在这个任务期间,队列中不会有其他任务执行
queue.async(flags: .barrier) {
print("Barrier task")
print("Barrier task2") // 在屏障任务中执行第二个打印
}
// 异步执行任务3,等待 barrier 任务执行完毕后继续
queue.async {
print("Task 3")
}
输出顺序:
• Task 1 和 Task 2 可能无序输出,因为它们是并发执行的。
• 屏障任务会在之前的任务完成后执行。
• Task 3 将在屏障任务结束后执行。
总结:
参考:
iOS - 线程中常见的几种锁_unlock tryluck-CSDN博客