谈谈 swift 中的线程安全

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

前面讲了 swift 中的死锁,今天顺便聊聊 swift 中的线程安全相关的话题。

讲讲 iOS 中的死锁

多线程并发是程序员都会用到的技能,但是因为调试和发现问题比较困难,所以往往最复杂和最奇怪的错误是由多线程并发引起的,这就需要我们在开发时特别注意。

在日常的开发中我们在处理多线程时应该多留个心眼,因为只要有多线程就会涉及到安全隐患。因此在本文中,我们将了解什么是线程安全,以及 iOS 提供了哪些工具来帮助我们实现它。

什么是线程安全?

线程安全指的是多个线程可能同时操作同一块内存,从而导致的异常情况,先举个例子:

swift 复制代码
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,这是因为让两个线程尝试同时操作同一个内存地址导致的。

解决竞争问题

由于是两个线程同时操作 user 导致的问题,那么我们想要解决,只需要让两个线程先后进行操作就行了,当线程 1 进行操作时,先上一把锁,锁住 user 不让线程 2 操作,等线程 1 操作完成,解开锁,再让线程 2 操作:

csharp 复制代码
let queue1 = DispatchQueue(label: "q1")
let queue2 = DispatchQueue(label: "q2")

private let lock = NSLock()
queue1.async {
    lock.lock()
    user.setName("1")
    print(user.name)
    lock.unlock()
}
queue2.async {
    lock.lock()
    user.setName("2")
    print(user.name)
    lock.unlock()
}

这下打印就正常了,保证了线程安全。

其他并发问题

除了上边提到的资源竞争问题,在使用并发的时候还可能导致一些其他问题,也需要注意,比如:

  • 条件竞争:无法同步执行两个或多个线程,导致事件以错误的顺序执行

  • 死锁:两个线程相互等待,这意味着两者都无法继续,线程会卡死

  • 优先级倒置:低优先级任务持有高优先级任务所需的资源,导致执行延迟

  • 线程爆炸:程序中申请的线程数量过多,导致资源耗尽和系统性能下降

  • 线程匮乏:因为其他线程正在占用这个资源,导致其他线程无法访问,从而导致执行延迟

iOS 中处理线程安全提供的 API

在 Swift 中,有许多不同的方法可以实现线程安全。具体要使用哪个 API 取决于你面临的问题,下边介绍一下这些 API,并提供一些示例来展示它们应该用于什么情况。

1、async/await

这个 API 在之前的文章中有介绍过:async/await

这个 API 是 swift 官方支持的,能够解决死锁、线程爆炸和数据竞争的问题,合理利用能够减少线程相关的问题,但不是杜绝,使用不合理的话一些逻辑错误仍有可能发生。而且该功能 iOS 15 开始才能使用,所以老项目目前还用不上。

2、使用串行队列

因为是多线程导致的问题,那么改成单线程,串行执行就能解决问题了, 比如上边的例子,改成单线程就可以了:

scss 复制代码
let queue1 = DispatchQueue(label: "q1")

queue1.async {
    user.setName("1")
    print(user.name)
}
queue1.async {
    user.setName("2")
    print(user.name)
}

但是,有时候为了代码执行效率之类的衡量,又必须使用多线程,这时候这种方案就行不通了。

3、使用锁

我们最开始的解决方案就是用锁来解决的,锁在 iOS 中也提供了多种选择,比如:

  • pthread_mutex_t 一种阻塞锁,常见的表现形式是当前线程会进入休眠

  • pthread_rwlock_t 阻塞读写锁

  • DispatchQueue 可以用作阻塞锁,也可以封装为读写锁

  • OperationQueue 可以用作阻塞锁

  • NSLock Objective-C 类的阻塞锁

  • OSSpinLock 自旋锁,使用一个无限循环直到锁被释放

我们一开始使用的解决方案就是 NSLock,它 API 简单,易于使用且效率很高。

4、使用信号量

信号量(DispatchSemaphore)其实是一种可用来控制访问资源的数量的标识,它的原理类似自旋锁,当发起 wait 时,会先锁住资源不让其他人访问,直到释放信号量 signal:

scss 复制代码
let semaphore = DispatchSemaphore(value: 1)
queue1.async {
    semaphore.wait()
    user.setName("1")
    print(user.name)
    semaphore.signal()
}
queue2.async {
    semaphore.wait()
    user.setName("2")
    print(user.name)
    semaphore.signal()
}

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 "iOS新知",每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

相关推荐
黑化旺仔16 小时前
【OC】Runtime:类元数据、加载初始化与消息派发
开发语言·macos·ios·objective-c
songgeb17 小时前
iOS App支持设置RTL语言的实践
ios·swift·ui kit
软泡芙17 小时前
【iOS】swift启动屏显示黑屏
ios
用户693717500138419 小时前
当你向领导申请 AI 编程工具预算时,如果领导追问“能提效多少,有量化数据吗”,你该怎么回答?
android·后端·ios
Layer19 小时前
一些脱离 Mac 依赖的 Agent 操作 iPhone 实现
ios·ai编程·测试
用户693717500138421 小时前
当我说 Claude 搭配 GLM-5.3-Flash 太好用了,同事却提醒我:你可能正在失去对代码的掌控
android·后端·ios
茶底世界之下21 小时前
一键裁方会丢掉什么?用 Awaking 给园林照片做两种画幅
ios
Marco_Marco1351 天前
定位SDK接入实战:Android/iOS/鸿蒙三端怎么选、怎么接
android·ios·harmonyos
老李IT笔记2 天前
关掉设备上的 AI 扩展:两个键各管一层,配错了等于没配
人工智能·ios·智能手机
2501_915921432 天前
iOS应用耗电测试:使用Energy Log、Keymob和Instruments进行性能监控与能源消耗分析
android·ios·小程序·https·uni-app·iphone·webview