谈谈 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新知",每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

相关推荐
2501_915106322 小时前
如何查看手机使用记录:Android和iOS设备全面指南
android·ios·智能手机·小程序·uni-app·iphone·webview
0xAaron9 小时前
确定crash文件和dSYM是否对应
ios·uuid·crash·dsym
0xAaron11 小时前
符号表和 dSYM UUID 确认
ios·cocoa·uuid·符号表·dsym
0xAaron11 小时前
如何使用dSYM文件来符号化崩溃信息
ios·swift·调试·崩溃·符号化·dsym
Swift社区13 小时前
在 Swift 中使用 Image Playground 生成 AI 图像:完整实战指南
开发语言·人工智能·swift
2501_9159184113 小时前
Flutter 加固方案全解析,从 Dart 层到 IPA 成品的多工具协同防护体系
flutter·macos·ios·小程序·uni-app·cocoa·iphone
vivo互联网技术13 小时前
从不足到精进:H5即开并行加载方案的演进之路
前端·h5·webview·客户端·大前端
wsxlgg14 小时前
IOS 打包上传提示you do not have required contracts to perform an operation
ios
每周报刊14 小时前
初代 iPhone SE 谢幕:被标为 “过时”,小屏旗舰时代彻底落幕
ios·iphone
RollingPin14 小时前
iOS 动态库与静态库的区别
ios·framework·动态库·静态库·符号表·三方库·dyld