建设网站需要的硬件设备,电子商务网站管理的基本内容,专业的建设企业网站公司,太原视频剪辑培训机构哪个好什么是锁#xff1f;
在程序中#xff0c;当多个任务#xff08;或线程#xff09;同时访问同一个资源时#xff0c;比如多个操作同时修改一份数据#xff0c;可能会导致数据不一致。这时候#xff0c;我们需要“锁”来确保同一时间只有一个任务能够操作这个数据#…什么是锁
在程序中当多个任务或线程同时访问同一个资源时比如多个操作同时修改一份数据可能会导致数据不一致。这时候我们需要“锁”来确保同一时间只有一个任务能够操作这个数据避免“抢占”问题。简单来说锁就是一种机制它能帮助你控制多个任务按顺序来操作资源。 按照锁的功能来进行分类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 1print(计数器增加: \(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 1print(计数器增加: \(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 1print(计数器增加: \(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 来实现的。条件变量有点像信号量提供了线程阻塞与信号机制因此可以用来阻塞某个线程并等待某个数据就绪随后唤醒线程比如常见的生产者-消费者模式。生产者-消费者模式可以查看我的另一篇博客iOS--生产者-消费者模式理解附GCD信号量代码实现-CSDN博客
let condition NSCondition()
var isReady falsefunc producer() {condition.lock()isReady truecondition.signal() // 唤醒等待中的线程condition.unlock()
}func consumer() {condition.lock()while !isReady {condition.wait() // 等待条件满足}// 执行消费操作condition.unlock()
}模拟
import Foundationlet condition NSCondition()
var isReady false// 生产者方法
func producer() {condition.lock()print(生产者正在准备资源...)isReady trueprint(资源准备完成通知消费者)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 将在屏障任务结束后执行。 总结 线程安全
上述学习的各种常见的线程锁都是为了更好地管理和调配线程而在开发中我们往往不可避免地采用多线程并发这虽然很便利但是存在的巨大的安全隐患。 什么是线程安全
线程安全指的是多个线程可能同时操作同一块内存从而导致的异常情况先举个例子
class User {private(set) var name: String func setName(_ name: String) {self.name name}
}let user User()let queue1 DispatchQueue(label: q1)
let queue2 DispatchQueue(label: q2)queue1.async {user.setName(1)print(user.name)
}
queue2.async {user.setName(2)print(user.name)
}
这段代码因为多线程并发同时修改同一个变量导致可能的结果是测试下来可能会是打印了两个 2这就不符合我们的预期了明明第一个 user.setName 传入的是 “1”打印结果却为 2。
这种情况称为资源竞争两个线程可能同时操作 user 对象实际上除了结果不符合预期外还可能出现一个经典的崩溃 EXC_BAD_ACCESS这是因为让两个线程尝试同时操作同一个内存地址导致的。 如何解决资源竞争问题
很简单用我们刚学完的锁就可以。这里就不举例子了。。。 其他并发问题
除了上边提到的资源竞争问题在使用并发的时候还可能导致一些其他问题也需要注意比如
条件竞争无法同步执行两个或多个线程导致事件以错误的顺序执行死锁两个线程相互等待这意味着两者都无法继续线程会卡死优先级倒置低优先级任务持有高优先级任务所需的资源导致执行延迟线程爆炸程序中申请的线程数量过多导致资源耗尽和系统性能下降线程匮乏因为其他线程正在占用这个资源导致其他线程无法访问从而导致执行延迟 解决方法
1. 条件竞争
使用锁使用互斥锁如NSLock或信号量如DispatchSemaphore确保对共享资源的安全访问。使用队列使用串行队列或DispatchQueue的同步方法来控制对共享数据的访问顺序。
2. 死锁
避免嵌套锁尽量避免一个线程在持有锁时请求另一个锁。设置锁的顺序确保所有线程按照相同的顺序获取锁。使用超时设置锁的获取超时防止无限等待。
3. 优先级倒置
优先级提升在需要时提升低优先级线程的优先级让所需资源尽快释放。资源管理确保高优先级线程能及时访问所需资源比如使用锁机制。
4. 线程爆炸
限制线程数量使用线程池管理线程数量避免创建过多线程。使用异步任务采用GCD的队列减少线程的创建。
5. 线程匮乏
优化资源使用检查资源访问和锁的使用确保高效利用资源。调整线程设计使用更灵活的线程模型比如异步编程减少对共享资源的依赖。 这里重点讲解一下什么是死锁。。。。。
死锁
在 Swift 中当两个线程都在等待对方释放资源时就会发生deadlock 死锁。这会导致线程都处于永久等待状态当主线程死锁应用的表现上就是崩溃其他子线程死锁可能导致卡死。
举个例子
class ViewController: UIViewController {override func viewDidLoad() {super.viewDidLoad()print(1)DispatchQueue.main.sync {print(2)}print(3)}
}
上述代码会输出1后程序崩溃。
原因主线程是处理用户界面和交互的线程。在viewDidLoad中打印1后想要在主线程上执行另一个任务打印2。使用sync意味着希望这个任务立刻完成而主线程正在执行viewDidLoad方法。因为主线程正在等着这个新任务完成打印2但这个任务又在主线程上运行所以主线程无法继续导致死锁。 再举一个因为互斥锁而引发死锁的例子
import Foundation// 创建两个锁
let lock1 NSLock()
let lock2 NSLock()// 线程1任务
func thread1() {print(线程1 尝试获取 lock1)lock1.lock()print(线程1 获取了 lock1)// 模拟处理一些操作sleep(1)print(线程1 尝试获取 lock2)lock2.lock() // 死锁发生在这里因为线程2已经持有了 lock2print(线程1 获取了 lock2) // 这行永远不会被执行lock2.unlock()lock1.unlock()
}// 线程2任务
func thread2() {print(线程2 尝试获取 lock2)lock2.lock()print(线程2 获取了 lock2)// 模拟处理一些操作sleep(1)print(线程2 尝试获取 lock1)lock1.lock() // 死锁发生在这里因为线程1已经持有了 lock1print(线程2 获取了 lock1) // 这行永远不会被执行lock1.unlock()lock2.unlock()
}// 并发队列
let queue DispatchQueue.global()// 启动线程
queue.async {thread1()
}queue.async {thread2()
} 线程1 首先获取 lock1然后等待获取 lock2。 线程2 首先获取 lock2然后等待获取 lock1。 由于两个线程互相等待对方释放锁导致程序进入死锁两个线程都无法继续执行。 造成死锁的四个条件 互斥条件某个资源一次只能被一个线程占用。占有且等待一个线程占有一个资源同时等待其他资源。不可剥夺线程所持有的资源不能被强制剥夺。循环等待两个或多个线程形成一种循环等待关系。 解决死锁的策略 避免锁的循环等待通过统一的锁顺序确保线程不会互相等待对方的资源。 使用超时机制锁请求可以设置超时时间防止无限等待。 使用NSRecursiveLock递归锁允许同一线程多次获取同一把锁避免递归调用中的死锁问题。 参考
iOS - 线程中常见的几种锁_unlock tryluck-CSDN博客
谈谈 swift 中的线程安全 - 知乎 (zhihu.com)
讲讲 iOS 中的死锁 (qq.com)