多线程

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

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

卖车票

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容

  • iOS多线程实践中,常用的就是子线程执行耗时操作,然后回到主线程刷新UI。在iOS中每个进程启动后都会建立一个主线...
    jackyshan阅读 1,450评论 2 12
  • 本文首发于我的个人博客:「程序员充电站」[https://itcharge.cn]文章链接:「传送门」[https...
    ITCharge阅读 348,034评论 308 1,926
  • 目录 一、基本概念1.多线程2.串行和并行, 并发3.队列与任务4.同步与异步5.线程状态6.多线程方案 二、GC...
    BohrIsLay阅读 1,583评论 5 12
  • [摘要]有的孩子天生不怕生,有的孩子面对亲人外的陌生人躲躲闪闪就像一只受惊的小鸟。家长都希望孩子生的落落大方,小的...
    第6通道阅读 169评论 0 0
  • 做自己喜欢的事,提升自己的能量振频。今天带着老宋和老杨(老宋54岁,老杨61岁)跑山,路线按照自己的感觉走,从来没...
    零贰O2阅读 191评论 0 0