谈谈 swift 中的线程安全

1,396 阅读4分钟

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

前言

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

讲讲 iOS 中的死锁

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

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

什么是线程安全?

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


class User {
    private(set) var name: String = ""
    func setName(_ nameString) {
        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 操作

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、使用串行队列

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

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

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