多线程

上一篇:协议
当前篇:多线程

随着多核多线程处理器的出现,多线程编程技术也已经普及,我们之前写的代码都是在单一线程中执行,实际应用中的任务会复杂很多,如果所有任务都在一个线程中去执行,应用必然会很卡

卖车票

我们用一个售票系统来模拟一下多线程编程

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)
}

上一篇:协议
当前篇:多线程

你可能感兴趣的:(多线程)