上一篇:协议
当前篇:多线程
随着多核多线程处理器的出现,多线程编程技术也已经普及,我们之前写的代码都是在单一线程中执行,实际应用中的任务会复杂很多,如果所有任务都在一个线程中去执行,应用必然会很卡
卖车票
我们用一个售票系统来模拟一下多线程编程
func lessonRun() {
var tickets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
while !tickets.isEmpty {
let ticket = tickets.removeFirst()
print("票\(ticket) 已经出售,剩余\(tickets.count)张票")
}
}
在调试框中可以看到票瞬间就卖完了,为了模拟现实的情况,假设售票员买一张票需要2秒
func lessonRun() {
var tickets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
while !tickets.isEmpty {
let ticket = tickets.removeFirst()
print("票\(ticket) 已经出售,剩余\(tickets.count)张票")
Thread.sleep(forTimeInterval: 2)
}
}
现在,这10张票就得等好一会儿才卖完,1个窗口卖得太慢了,那么我们再新增一个窗口是不是就快一些呢
func lessonRun() {
var tickets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
while !tickets.isEmpty {
let ticket = tickets.removeFirst()
print("窗口1售出 票\(ticket),剩余\(tickets.count)张票")
Thread.sleep(forTimeInterval: 2)
}
while !tickets.isEmpty {
let ticket = tickets.removeFirst()
print("窗口2售出 票\(ticket),剩余\(tickets.count)张票")
Thread.sleep(forTimeInterval: 2)
}
}
观察运行结果,实际上并没有什么用,为什么呢,因为这两个循环语句是在同一个线程严格按照从上到下的顺序执行,第二个循环开始执行的时候,第一个循环已经把票卖完了,没有起到多线程的作用,仔细看下面的代码
func lessonRun() {
var tickets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
DispatchQueue.global().async {
while !tickets.isEmpty {
let ticket = tickets.removeFirst()
print("窗口1售出 票\(ticket),剩余\(tickets.count)张票")
Thread.sleep(forTimeInterval: 2)
}
}
DispatchQueue.global().async {
while !tickets.isEmpty {
let ticket = tickets.removeFirst()
print("窗口2售出 票\(ticket),剩余\(tickets.count)张票")
Thread.sleep(forTimeInterval: 2)
}
}
}
观察运行结果,两个窗口同时卖起了票,如果你发现卖票的结果有问题,先不用管,我们稍后会讲,总之我们第一次看见两个任务在同时进行了,第一次多线程编程就成功了,我们一起来认识一下 Grand Central Dispatch ,简称 GCD
GCD
GCD 是苹果为多核并行运算提出的解决方案,它会自动利用多核多线程进行并发运算,同时它会自动管理线程的生命周期,我们要做的就是为我们的任务选择执行的方式
GCD 强大的地方在于,我们不在需要关注线程这个概念,过去做多线程开发,首先要创建一个线程对象,然后要开启线程,为线程指派任务,当任务结束后要关闭线程,释放线程对象,而使用 GCD ,这些过程全部被隐藏了起来,我们只需要关注两个概念:队列 和 任务
将上述代码拆解一下, DispatchQueue 就是队列,大括号括起来的代码就是任务,队列用于管理任务的执行,队列分两种,一种是 串行队列 ,一种是 并行队列 ,任务执行的方式也有两种,一种是同步执行 sync ,一种是异步执行 async
串行队列: 按照先后顺序一个一个地执行其中的任务,上一个任务执行结束后才执行下一个任务,串行队列只在一个线程中执行
并行队列: 在同步执行方式下,不会开启多线程,在异步执行方式下,会开启多个线程同时执行其中的任务
func lessonRun() {
var tickets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
DispatchQueue.global().sync {
while !tickets.isEmpty {
let ticket = tickets.removeFirst()
print("窗口1售出 票\(ticket),剩余\(tickets.count)张票")
Thread.sleep(forTimeInterval: 2)
}
}
DispatchQueue.global().async {
while !tickets.isEmpty {
let ticket = tickets.removeFirst()
print("窗口2售出 票\(ticket),剩余\(tickets.count)张票")
Thread.sleep(forTimeInterval: 2)
}
}
}
将第一个队列的执行方式改为同步 sync 后观察运行结果,跟之前单线程的结果一致,不代表这样就不是多线程,而是 sync 同步执行最大的区别是,它需要等待当前任务结束才执行后面的任务,因此在开发中实际上几乎不会用到 sync 这种执行方式
主队列和全局队列
iOS应用由很多图形界面构成,我们叫 UI ,按照 iOS 系统的设计,UI 任务必须严格按照先后顺序执行,否则界面绘制会出现无法预知的错误,因此 GCD 为我们提供了一个特有的队列,所有 UI 任务必须在主队列中执行,否则应用可能会崩溃,访问主队列的方式 DispatchQueue.main ,显然这个队列是串行队列,而全局队列就是上面的代码中用到的 DispatchQueue.global() ,全局队列是并行队列
简单起见,在 iOS 开发中,需要保证 UI 任务在主队列中执行,当我们遇到了耗时的任务时,可以用全局队列来执行,例如当票卖完的时候,要在主队列中打印票卖完的信息:
func lessonRun() {
var tickets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
DispatchQueue.global().async {
while !tickets.isEmpty {
let ticket = tickets.removeFirst()
print("窗口1售出 票\(ticket),剩余\(tickets.count)张票")
Thread.sleep(forTimeInterval: 2)
}
DispatchQueue.main.async {
print("票卖完了")
}
}
DispatchQueue.global().async {
while !tickets.isEmpty {
let ticket = tickets.removeFirst()
print("窗口2售出 票\(ticket),剩余\(tickets.count)张票")
Thread.sleep(forTimeInterval: 2)
}
DispatchQueue.main.async {
print("票卖完了")
}
}
}
多线程互斥
之前我们还留了一个问题没解,那就是在两个窗口同时卖票的情况下出现了混乱的情况,有的票卖了几次,这是为什么呢,因为两个同时进行的任务访问了同一个数据源,也就是票数组,就好像两个窗口是在同一个票盒子里取的票,当用户来买票时,售票员1看了下票盒子,告诉客户票1卖给你,然后就去办手续了,同时售票员2接待客户,看了下票盒子告诉客户票1卖给你,然后就去办手续了,票就全乱了
虽然这种情况在现实中不会发生,但是在计算机中就会发生,如何避免,我们可以给票盒子装一把锁,配一把钥匙,售票员卖票时先拿钥匙去开锁,如果钥匙被人拿了,就必须等一会儿,拿到钥匙的售票员可以打开票盒子,卖票完成后把盒子锁上,把钥匙放回去,这样就避免了冲突,这样在现实中看起来太繁琐,但是对于计算机来说很快,而且是必须的
func lessonRun() {
var tickets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let lock = NSLock()
DispatchQueue.global().async {
while !tickets.isEmpty {
lock.lock() //卖票前先拿钥匙上锁,如果锁被占用了,需要等待
let ticket = tickets.removeFirst()
print("窗口1售出 票\(ticket),剩余\(tickets.count)张票")
lock.unlock() //卖完票后把钥匙还回去
Thread.sleep(forTimeInterval: 2)
}
DispatchQueue.main.async {
print("票卖完了")
}
}
DispatchQueue.global().async {
while !tickets.isEmpty {
lock.lock()
let ticket = tickets.removeFirst()
print("窗口2售出 票\(ticket),剩余\(tickets.count)张票")
lock.unlock()
Thread.sleep(forTimeInterval: 2)
}
DispatchQueue.main.async {
print("票卖完了")
}
}
}
这样卖票程序就正常运行了,lock 相当于 tickets 的一个开关,售票员在访问 tickets 之前首先需要获取开关,如果开关被别人获取了,就需要等待,当获取到开关后开始卖票,卖票结束后把开关退还
把代码稍微改一下,让票卖快一点
func lessonRun() {
var tickets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let lock = NSLock()
func openSaleWindow(number: Int) {
DispatchQueue.global().async {
while !tickets.isEmpty {
lock.lock()
let ticket = tickets.removeFirst()
print("窗口\(number)售出 票\(ticket),剩余\(tickets.count)张票")
lock.unlock()
Thread.sleep(forTimeInterval: 2)
}
DispatchQueue.main.async {
print("票卖完了")
}
}
}
openSaleWindow(number: 1)
openSaleWindow(number: 2)
openSaleWindow(number: 3)
openSaleWindow(number: 4)
openSaleWindow(number: 5)
}
任务延迟
任务可以延迟一段时间执行,方式很简单
func lessonRun() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
print("这是一个在主队列中延迟2秒执行的任务")
}
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + 2.5) {
print("这是一个在全局队列中延迟2秒执行的任务")
}
}
定时器
如果我们需要每隔一段时间执行一次任务,可以用定时器来实现,下面我们把卖车票的例子改成用定时器来实现
func lessonRun() {
var tickets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let lock = NSLock()
func openSaleWindow(number: Int) {
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
timer.schedule(deadline: DispatchTime.now(), repeating: 2)
timer.setEventHandler {
lock.lock()
if tickets.isEmpty {
lock.unlock()
timer.cancel()
return
}
let ticket = tickets.removeFirst()
print("窗口\(number)售出 票\(ticket),剩余\(tickets.count)张票")
lock.unlock()
}
timer.resume()
}
openSaleWindow(number: 1)
openSaleWindow(number: 2)
openSaleWindow(number: 3)
openSaleWindow(number: 4)
openSaleWindow(number: 5)
}
上一篇:协议
当前篇:多线程